/*-------------------------------------------------------------------------
 *
 * multixact.c
 *		PostgreSQL 多事务日志管理器
 *
 * pg_multixact 管理器是一个类似 pg_xact 的管理器，用于存储每个 MultiXactId 的
 * MultiXactMember 数组。它是共享行锁实现的重要部分。每个 MultiXactMember 由一个
 * TransactionId 和一组标志位组成。这个名称有点历史渊源：
 * 最初，MultiXactId 由多个 TransactionId 组成（除了在极少数特殊情况下），
 * 因此叫“多重”。但是，如今只包含单个 Xid 的 MultiXactIds 是完全合法的。
 *
 * 这些标志位的含义对本模块是模糊的，但它们主要用于 heapam.c 中，以识别每个
 * 会员事务在任何给定元组上持有的锁模式。本模块仅包含支持存储和检索数组的功能。
 *
 * 我们使用两个 SLRU 区域，一个用于存储其他区域中每个 MultiXactId 的数据起始
 * 偏移。这一技巧使我们能够存储可变长度的 TransactionIds 数组。
 * （我们也可以使用一个区域，其中包含计数和 TransactionIds，有效的 MultiXactId
 * 值指向包含计数的槽；但那种方式似乎不太可靠，因为如果有人询问一个指向
 * 包含 XID 的中间槽的虚假 MultiXactId，那将会完全混乱。）
 *
 * XLOG 交互：本模块每当一个新的 OFFSETs 或 MEMBERs 页面被初始化为零时都会
 * 生成一个记录，以及每当定义一个新的 MultiXactId 时生成一个 XLOG_MULTIXACT_CREATE_ID 记录。 
 * 本模块忽略 WAL 规则“写 XLOG 在数据之前”，因为记录一个 MultiXactId 的操作
 * 在 heap xmax 中遵循该规则是足够的。唯一的方式是通过 heap_lock_tuple() 
 * 或 heap_update() 将 MXID 引用到任何数据页面中，而这每个都生成一个必须遵循
 * 我们的 XLOG 记录。数据页面与 XLOG 记录之间的正常 LSN 锁定将确保我们的 XLOG 
 * 记录首先达到磁盘。如果 SLRU 会员/偏移数据早于 XLOG 记录达到磁盘，我们不在乎；
 * 恢复后，任何 xmax 都不会引用它。在另一面，为确保所有引用的条目确实到达
 * 磁盘，本模块的 XLOG 记录会完全重建自最后一个检查点以来输入的数据。
 * 每次检查点被认为完整之前，我们都会将所有脏 OFFSETs 和 MEMBERs 页面刷新并
 * 同步到磁盘。
 *
 * 与 clog.c 类似，而与 subtrans.c 不同，我们必须在崩溃的情况下保持状态
 * 并确保 MXID 和偏移量编号在崩溃中单调增加。我们以与事务 ID 相同的方式处理
 * 这一点：WAL 记录保证包含我们可能需要担心的每个 MXID 的证据，并且我们仅确保
 * 在重放结束时，next-MXID 和 next-offset 计数器至少与在重放过程中看到的
 * 任何计数一样大。
 *
 * 我们能够通过仔细跟踪每个表的使用值来移除不再必要的段：在清理期间，任何
 * 多事务如果早于某个值将被移除；截止值存储在 pg_class 中。每个数据库中
 * 所有表的最小值存储在 pg_database 中，而所有数据库的全局最小值是 pg_control
 * 的一部分并保存在共享内存中。每当最小值被提升时，SLRUs 将被截断。
 *
 * 当要创建新的多事务 ID 值时，会特别注意使计数器不落入包围地平线，
 * 考虑全局最小值。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/backend/access/transam/multixact.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/multixact.h"
#include "access/slru.h"
#include "access/transam.h"
#include "access/twophase.h"
#include "access/twophase_rmgr.h"
#include "access/xact.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
#include "catalog/pg_type.h"
#include "commands/dbcommands.h"
#include "funcapi.h"
#include "lib/ilist.h"
#include "miscadmin.h"
#include "pg_trace.h"
#include "postmaster/autovacuum.h"
#include "storage/lmgr.h"
#include "storage/pmsignal.h"
#include "storage/proc.h"
#include "storage/procarray.h"
#include "utils/builtins.h"
#include "utils/memutils.h"
#include "utils/snapmgr.h"


/*
 * 多重事务偏移页大小的定义。 页的大小与Postgres中其他地方使用的BLCKSZ相同。
 *
 * 注意：由于MultiXactOffsets是32位并在0xFFFFFFFF处环绕， 
 * 多重事务页编号也在0xFFFFFFFF/MULTIXACT_OFFSETS_PER_PAGE处环绕， 
 * 段编号在0xFFFFFFFF/MULTIXACT_OFFSETS_PER_PAGE/SLRU_PAGES_PER_SEGMENT处环绕。 
 * 我们在这个模块中不需要明确注意这一事实， 
 * 除非在TruncateMultiXact中比较段号和页号时（参见 
 * MultiXactOffsetPagePrecedes）。
 */

/* 我们每个偏移需要四个字节 */
#define MULTIXACT_OFFSETS_PER_PAGE (BLCKSZ / sizeof(MultiXactOffset))

#define MultiXactIdToOffsetPage(xid) \
	((xid) / (MultiXactOffset) MULTIXACT_OFFSETS_PER_PAGE)
#define MultiXactIdToOffsetEntry(xid) \
	((xid) % (MultiXactOffset) MULTIXACT_OFFSETS_PER_PAGE)
#define MultiXactIdToOffsetSegment(xid) (MultiXactIdToOffsetPage(xid) / SLRU_PAGES_PER_SEGMENT)

/*
 * 成员的情况稍微复杂一些：我们为每个TransactionId存储一个字节的 
 * 额外标志位。 为了避免对齐问题，我们存储四个字节的标志，
 * 然后是对应的4个Xids。 我们将每个这样的5个字（20字节）集称为“组”，
 * 并将其作为一个整体存储在页面中。 因此，使用8kB的BLCKSZ，
 * 我们每页保持409个组。 这每页浪费12个字节，但没关系——
 * 简单性（和性能）胜过空间效率。
 *
 * 请注意，“偏移”宏是以字节偏移工作，而不是数组索引，因此 
 * 算术运算必须使用“char *”指针进行。
 */
/* 每个事务需要八位，因此一个事务适合一个字节 */
#define MXACT_MEMBER_BITS_PER_XACT			8
#define MXACT_MEMBER_FLAGS_PER_BYTE			1
#define MXACT_MEMBER_XACT_BITMASK	((1 << MXACT_MEMBER_BITS_PER_XACT) - 1)

/* 每组中有多少个完整的字节标志？ */
#define MULTIXACT_FLAGBYTES_PER_GROUP		4
#define MULTIXACT_MEMBERS_PER_MEMBERGROUP	\
	(MULTIXACT_FLAGBYTES_PER_GROUP * MXACT_MEMBER_FLAGS_PER_BYTE)
/* 完整组的字节大小 */
#define MULTIXACT_MEMBERGROUP_SIZE \
	(sizeof(TransactionId) * MULTIXACT_MEMBERS_PER_MEMBERGROUP + MULTIXACT_FLAGBYTES_PER_GROUP)
#define MULTIXACT_MEMBERGROUPS_PER_PAGE (BLCKSZ / MULTIXACT_MEMBERGROUP_SIZE)
#define MULTIXACT_MEMBERS_PER_PAGE	\
	(MULTIXACT_MEMBERGROUPS_PER_PAGE * MULTIXACT_MEMBERS_PER_MEMBERGROUP)

/*
 * 由于每页的项目数量不是最后一个项目编号（成员0xFFFFFFFF）的除数， 
 * 因此最后的段没有使用最大数量的页面， 
 * 而且最后使用的页面中的项目数量与之前的页面也不同。 
 * （另一个说法是0xFFFFFFFF成员位于最后页面的中间，所以该页面在该项目之后有一些空白。）
 *
 * 这个常量是最后段的最后页面中的成员数量。
 */
#define MAX_MEMBERS_IN_LAST_MEMBERS_PAGE \
		((uint32) ((0xFFFFFFFF % MULTIXACT_MEMBERS_PER_PAGE) + 1))

/* 可以找到成员的页面 */
#define MXOffsetToMemberPage(xid) ((xid) / (TransactionId) MULTIXACT_MEMBERS_PER_PAGE)
#define MXOffsetToMemberSegment(xid) (MXOffsetToMemberPage(xid) / SLRU_PAGES_PER_SEGMENT)

/* 给定成员的标志字的位置（页面内字节偏移） */
#define MXOffsetToFlagsOffset(xid) \
	((((xid) / (TransactionId) MULTIXACT_MEMBERS_PER_MEMBERGROUP) % \
	  (TransactionId) MULTIXACT_MEMBERGROUPS_PER_PAGE) * \
	 (TransactionId) MULTIXACT_MEMBERGROUP_SIZE)
#define MXOffsetToFlagsBitShift(xid) \
	(((xid) % (TransactionId) MULTIXACT_MEMBERS_PER_MEMBERGROUP) * \
	 MXACT_MEMBER_BITS_PER_XACT)

/* 给定成员的TransactionId的位置（页面内字节偏移） */
#define MXOffsetToMemberOffset(xid) \
	(MXOffsetToFlagsOffset(xid) + MULTIXACT_FLAGBYTES_PER_GROUP + \
	 ((xid) % MULTIXACT_MEMBERS_PER_MEMBERGROUP) * sizeof(TransactionId))

/* 多重事务成员环绕阈值。 */
#define MULTIXACT_MEMBER_SAFE_THRESHOLD		(MaxMultiXactOffset / 2)
#define MULTIXACT_MEMBER_DANGER_THRESHOLD	\
	(MaxMultiXactOffset - MaxMultiXactOffset / 4)

#define PreviousMultiXactId(xid) \
	((xid) == FirstMultiXactId ? MaxMultiXactId : (xid) - 1)

/*
 * 指向多重事务控制的共享内存数据结构
 */
static SlruCtlData MultiXactOffsetCtlData;
static SlruCtlData MultiXactMemberCtlData;

#define MultiXactOffsetCtl	(&MultiXactOffsetCtlData)
#define MultiXactMemberCtl	(&MultiXactMemberCtlData)

/*
 * 跨所有后端共享的多重事务状态。 所有这些状态都受到MultiXactGenLock的保护。
 * （我们还使用MultiXactOffsetSLRULock和MultiXactMemberSLRULock来保护对这两组SLRU
 * 缓冲区的访问。 为了并发的理由，我们避免同时持有多个这些锁。）
 */
typedef struct MultiXactStateData
{
	/* 下一个待分配的MultiXactId */
	MultiXactId nextMXact;

	/* 下一个待分配的偏移量 */
	MultiXactOffset nextOffset;

	/* 我们是否完成了多重事务启动？ */
	bool		finishedStartup;

	/*
	 * 仍然可能被关系引用的最旧的多重事务。 
	 * 任何比这更旧的事务都不应该被查询。 这些值由vacuum更新。
	 */
	MultiXactId oldestMultiXactId;
	Oid			oldestMultiXactDB;

	/*  
	 * 可能被关系引用的多事务的最旧的多事务偏移量。我们并不总是知道这个值，因此这里有一个标志来指示我们当前是否知道。 
	 */  
	MultiXactOffset oldestOffset;
	bool		oldestOffsetKnown;

	/* 支持反环绕措施 */  
	MultiXactId multiVacLimit;
	MultiXactId multiWarnLimit;
	MultiXactId multiStopLimit;
	MultiXactId multiWrapLimit;

	/* 支持成员反环绕措施 */  
	MultiXactOffset offsetStopLimit;	/* 如果 oldestOffsetKnown 知道 */  

	/*
	 * 每个后端的数据从这里开始。我们有两个数组存储在 MultiXactStateData 结构后面的区域中。每个数组通过 BackendId 索引。
	 *
	 * 在两个数组中，所有正常后端（1..MaxBackends）后面有一个插槽用于 max_prepared_xacts 准备事务。有效的 BackendIds 从 1 开始；每个数组的元素零从不使用。
	 *
	 * OldestMemberMXactId[k] 是每个后端当前事务可能属于的最老的 MultiXactId，当后端没有可能成为 MultiXact 成员的活跃事务时，则为 InvalidMultiXactId。每个后端在首次获取给定事务中的共享锁之前，将其条目设置为当前的 nextMXact 计数器，并在事务结束时清除它。（这是有效的，因为只有在获取共享锁期间或之后，XID 才有可能成为 MultiXact 的成员，而该 MultiXact 必须在获取锁期间或之后创建。）
	 *
	 * OldestVisibleMXactId[k] 是每个后端当前事务认为可能是活跃的最老 MultiXactId，当不在事务中或不在尚未关注 MultiXacts 的事务中时，则为 InvalidMultiXactId。这个值在给定事务中首次需要时计算，并在事务结束时清除。我们可以在计算时将其作为有效的 OldestMemberMXactId[] 条目的最小值进行计算（如果没有有效的，则使用 nextMXact）。每个后端被要求不试图访问比其自身 OldestVisibleMXactId[] 设置更旧的 MultiXactId 的 SLRU 数据；这是必要的，因为检查点可能在任何时刻截断此类数据。
	 *
	 * 所有 OldestMemberMXactId[] 和 OldestVisibleMXactId[] 条目中最古老的有效值被 vacuum 视为仍然具有任何活跃成员事务的最早可能值。将 vacuum_multixact_freeze_min_age 从该值中减去，我们获得该表的 multixacts 冻结点。任何比这更旧的值从元组头中移除（或“冻结”；见 FreezeMultiXactId）。请注意，拥有比 xids 截断点更旧的成员 xids 的 multis 也必须冻结，即使这些 multis 自身比 multixid 截断点更新。每当发生完全表 vacuum 时，计算出的冻结点被用作新的 pg_class.relminmxid 值。数据库中所有这些值的最小值存储为 pg_database.datminmxid。反过来，所有这些值的最小值存储在 pg_control 中，并用作 pg_multixact 的截断点。在检查点或重启点，移除不需要的段。
 */
	MultiXactId perBackendXactIds[FLEXIBLE_ARRAY_MEMBER];
} MultiXactStateData;

/*
 * OldestMemberMXactId 和 OldestVisibleMXactId 数组的最后一个元素。
 * 有效元素是 (1..MaxOldestSlot)；元素 0 从不使用。
 */
#define MaxOldestSlot	(MaxBackends + max_prepared_xacts)

/* 指向共享内存中状态数据的指针 */
static MultiXactStateData *MultiXactState;
static MultiXactId *OldestMemberMXactId;
static MultiXactId *OldestVisibleMXactId;


/*
 * 后端本地 MultiXactId 缓存的定义。
 *
 * 我们使用这个缓存来存储已知的 MultiXact，因此我们不需要每次都去
 * SLRU 区域。
 *
 * 缓存持续时间为单个事务的持续时间，原因在于大多数条目将包含
 * 我们自己的 TransactionId，因此在我们的下一个事务开始时，它们
 * 将变得无趣。
 * （XXX 这是否正确尚不清楚 --- MultiXact 的其他成员可能会停留
 * 更长时间。不过，对于清缓存旧条目的更好策略尚不清楚。）FIXME 
 * 实际上，这在 multixact 可能包含更新 Xids 的情况下是完全错误的。
 *
 * 我们在一个在事务结束时删除的内存上下文中分配缓存条目，
 * 因此我们无需逐个释放条目。
 */
typedef struct mXactCacheEnt
{
	MultiXactId multi;
	int			nmembers;
	dlist_node	node;
	MultiXactMember members[FLEXIBLE_ARRAY_MEMBER];
} mXactCacheEnt;

#define MAX_CACHE_ENTRIES	256
static dlist_head MXactCache = DLIST_STATIC_INIT(MXactCache);
static int	MXactCacheMembers = 0;
static MemoryContext MXactContext = NULL;

#ifdef MULTIXACT_DEBUG
#define debug_elog2(a,b) elog(a,b)
#define debug_elog3(a,b,c) elog(a,b,c)
#define debug_elog4(a,b,c,d) elog(a,b,c,d)
#define debug_elog5(a,b,c,d,e) elog(a,b,c,d,e)
#define debug_elog6(a,b,c,d,e,f) elog(a,b,c,d,e,f)
#else
#define debug_elog2(a,b)
#define debug_elog3(a,b,c)
#define debug_elog4(a,b,c,d)
#define debug_elog5(a,b,c,d,e)
#define debug_elog6(a,b,c,d,e,f)
#endif

/* internal MultiXactId management */
static void fc_MultiXactIdSetOldestVisible(void);
static void fc_RecordNewMultiXact(MultiXactId fc_multi, MultiXactOffset fc_offset,
							   int fc_nmembers, MultiXactMember *fc_members);
static MultiXactId fc_GetNewMultiXactId(int fc_nmembers, MultiXactOffset *fc_offset);

/* MultiXact cache management */
static int	fc_mxactMemberComparator(const void *fc_arg1, const void *fc_arg2);
static MultiXactId fc_mXactCacheGetBySet(int fc_nmembers, MultiXactMember *fc_members);
static int	fc_mXactCacheGetById(MultiXactId fc_multi, MultiXactMember **fc_members);
static void fc_mXactCachePut(MultiXactId fc_multi, int fc_nmembers,
						  MultiXactMember *fc_members);

static char *fc_mxstatus_to_string(MultiXactStatus fc_status);

/* management of SLRU infrastructure */
static int	fc_ZeroMultiXactOffsetPage(int fc_pageno, bool fc_writeXlog);
static int	fc_ZeroMultiXactMemberPage(int fc_pageno, bool fc_writeXlog);
static bool fc_MultiXactOffsetPagePrecedes(int fc_page1, int fc_page2);
static bool fc_MultiXactMemberPagePrecedes(int fc_page1, int fc_page2);
static bool fc_MultiXactOffsetPrecedes(MultiXactOffset fc_offset1,
									MultiXactOffset fc_offset2);
static void fc_ExtendMultiXactOffset(MultiXactId fc_multi);
static void fc_ExtendMultiXactMember(MultiXactOffset fc_offset, int fc_nmembers);
static bool fc_MultiXactOffsetWouldWrap(MultiXactOffset fc_boundary,
									 MultiXactOffset fc_start, uint32 fc_distance);
static bool fc_SetOffsetVacuumLimit(bool fc_is_startup);
static bool fc_find_multixact_start(MultiXactId fc_multi, MultiXactOffset *fc_result);
static void fc_WriteMZeroPageXlogRec(int fc_pageno, uint8 fc_info);
static void fc_WriteMTruncateXlogRec(Oid fc_oldestMultiDB,
								  MultiXactId fc_startTruncOff,
								  MultiXactId fc_endTruncOff,
								  MultiXactOffset fc_startTruncMemb,
								  MultiXactOffset fc_endTruncMemb);


/*
 * MultiXactIdCreate
 *		构造一个表示两个 TransactionIds 的 MultiXactId。
 *
 * 这两个 XID 必须不同，或者请求不同的状态。
 *
 * 注意 - 我们在这里不担心我们的本地 MultiXactId 缓存，因为
 * 这由底层例程处理。
 */
MultiXactId MultiXactIdCreate(TransactionId fc_xid1, MultiXactStatus fc_status1,
				  TransactionId fc_xid2, MultiXactStatus fc_status2)
{
	MultiXactId fc_newMulti;
	MultiXactMember fc_members[2];

	AssertArg(TransactionIdIsValid(fc_xid1));
	AssertArg(TransactionIdIsValid(fc_xid2));

	Assert(!TransactionIdEquals(fc_xid1, fc_xid2) || (fc_status1 != fc_status2));

	/* MultiXactIdSetOldestMember() 必须已经被调用。 */
	Assert(MultiXactIdIsValid(OldestMemberMXactId[MyBackendId]));

	/*
	 * 注意：与 MultiXactIdExpand 不同，我们不麻烦检查两个 XID
	 * 是否仍在运行。在典型使用中，xid2 将是我们自己的 XID，
	 * 调用者刚刚检查了 xid1，因此这样做是多余的。
	 */

	fc_members[0].xid = fc_xid1;
	fc_members[0].status = fc_status1;
	fc_members[1].xid = fc_xid2;
	fc_members[1].status = fc_status2;

	fc_newMulti = MultiXactIdCreateFromMembers(2, fc_members);

	debug_elog3(DEBUG2, "Create: %s",
				mxid_to_string(newMulti, 2, members));

	return fc_newMulti;
}

/*
 * MultiXactIdExpand
 *		向预先存在的 MultiXactId 添加一个 TransactionId。
 *
 * 如果 TransactionId 已经是传递的 MultiXactId 的成员，
 * 且状态相同，直接返回原样。
 *
 * 请注意，我们实际上并不修改预先存在的 MultiXactId 的成员资格；
 * 相反，我们创建一个新的。这是为了解决尝试等待一个 MultiXactId
 * 完成的代码可能出现的竞争条件；参见 heapam.c 中的说明。
 *
 * 注意 - 我们在这里不担心我们的本地 MultiXactId 缓存，因为
 * 这由底层例程处理。
 *
 * 注意：来自旧集群（即通过 pg_upgrade 从比此功能更早的集群
 * 升级的集群）的 MultiXactIds 不能传入。
 */
MultiXactId MultiXactIdExpand(MultiXactId fc_multi, TransactionId fc_xid, MultiXactStatus fc_status)
{
	MultiXactId fc_newMulti;
	MultiXactMember *fc_members;
	MultiXactMember *fc_newMembers;
	int			fc_nmembers;
	int			fc_i;
	int			fc_j;

	AssertArg(MultiXactIdIsValid(fc_multi));
	AssertArg(TransactionIdIsValid(fc_xid));

	/* MultiXactIdSetOldestMember() 必须已经被调用。 */
	Assert(MultiXactIdIsValid(OldestMemberMXactId[MyBackendId]));

	debug_elog5(DEBUG2, "Expand: received multi %u, xid %u status %s",
				multi, xid, mxstatus_to_string(status));

	/*
	 * 注意：我们不允许旧的多重事务。原因在于调用此函数的
	 * 唯一调用者会检查多重事务是否不再运行。
	 */
	fc_nmembers = GetMultiXactIdMembers(fc_multi, &fc_members, false, false);

	if (fc_nmembers < 0)
	{
		MultiXactMember fc_member;

		/*
		 * 该 MultiXactId 已过时。这只能在所有 MultiXactId 成员
		 * 在调用者检查和传递给我们之间停止运行时发生。将这一
		 * 事实返回给调用者更好，但这会 complicate  API，且不
		 * 可能经常发生，因此只需通过创建一个单例 MultiXact 来
		 * 处理。
		 */
		fc_member.xid = fc_xid;
		fc_member.status = fc_status;
		fc_newMulti = MultiXactIdCreateFromMembers(1, &fc_member);

		debug_elog4(DEBUG2, "Expand: %u has no members, create singleton %u",
					multi, newMulti);
		return fc_newMulti;
	}

	/*
	 * 如果TransactionId已经是具有相同状态的MultiXactId的成员，则直接返回现有的MultiXactId。
	 */
	for (fc_i = 0; fc_i < fc_nmembers; fc_i++)
	{
		if (TransactionIdEquals(fc_members[fc_i].xid, fc_xid) &&
			(fc_members[fc_i].status == fc_status))
		{
			debug_elog4(DEBUG2, "Expand: %u is already a member of %u",
						xid, multi);
			pfree(fc_members);
			return fc_multi;
		}
	}

	/*
	 * 确定MultiXactId的哪些成员仍然是
	 * 相关的。这是任何正在运行的事务，以及任何抓取了比单纯锁更强的东西并且已提交的事务。（一个中止的更新在这里没有兴趣；并且在一个多事务中有多个更新Xid会在其他地方导致错误。）
	 *
	 * 移除死成员不仅仅是优化：冻结Xmax为multi的元组依赖于此行为。
	 *
	 * 注意我们在这里和上面有相同的竞争条件：j在循环结束时可能为0。
	 */
	fc_newMembers = (MultiXactMember *)
		palloc(sizeof(MultiXactMember) * (fc_nmembers + 1));

	for (fc_i = 0, fc_j = 0; fc_i < fc_nmembers; fc_i++)
	{
		if (TransactionIdIsInProgress(fc_members[fc_i].xid) ||
			(ISUPDATE_from_mxstatus(fc_members[fc_i].status) &&
			 TransactionIdDidCommit(fc_members[fc_i].xid)))
		{
			fc_newMembers[fc_j].xid = fc_members[fc_i].xid;
			fc_newMembers[fc_j++].status = fc_members[fc_i].status;
		}
	}

	fc_newMembers[fc_j].xid = fc_xid;
	fc_newMembers[fc_j++].status = fc_status;
	fc_newMulti = MultiXactIdCreateFromMembers(fc_j, fc_newMembers);

	pfree(fc_members);
	pfree(fc_newMembers);

	debug_elog3(DEBUG2, "Expand: returning new multi %u", newMulti);

	return fc_newMulti;
}

/*
 * MultiXactIdIsRunning
 *		返回一个MultiXactId是否“正在运行”。
 *
 * 如果给定的MultiXactId的至少一个成员仍在运行，我们将返回true。 注意，“false”的结果肯定不会改变，因为将成员添加到现有的MultiXactId中是不合法的。
 *
 * 调用者应验证多重事务不来自pg_upgraded共享锁定的元组。
 */
bool MultiXactIdIsRunning(MultiXactId fc_multi, bool fc_isLockOnly)
{
	MultiXactMember *fc_members;
	int			fc_nmembers;
	int			fc_i;

	debug_elog3(DEBUG2, "IsRunning %u?", multi);

	/*
	 * 这里的“false”意味着我们假设调用者已经检查过，给定的
	 * 多重事务不可能来自pg_upgraded数据库。
	 */
	fc_nmembers = GetMultiXactIdMembers(fc_multi, &fc_members, false, fc_isLockOnly);

	if (fc_nmembers <= 0)
	{
		debug_elog2(DEBUG2, "IsRunning: no members");
		return false;
	}

	/*
	 * 相比于查看共享内存，检查我自己是便宜的；
	 * 如果当前顶层事务的任何活动子事务是成员，则返回true。
	 *
	 * 这对于正确性不是必需的，只是一个快速路径。
	 */
	for (fc_i = 0; fc_i < fc_nmembers; fc_i++)
	{
		if (TransactionIdIsCurrentTransactionId(fc_members[fc_i].xid))
		{
			debug_elog3(DEBUG2, "IsRunning: I (%d) am running!", i);
			pfree(fc_members);
			return true;
		}
	}

	/*
	 * 通过在procarray.c中添加另一个入口点，可以使这更快，
	 * 仅为所有成员遍历PGPROC数组一次。但在大多数
	 * 情况下，nmembers应该足够小，这样就没那么重要了。
	 */
	for (fc_i = 0; fc_i < fc_nmembers; fc_i++)
	{
		if (TransactionIdIsInProgress(fc_members[fc_i].xid))
		{
			debug_elog4(DEBUG2, "IsRunning: member %d (%u) is running",
						i, members[i].xid);
			pfree(fc_members);
			return true;
		}
	}

	pfree(fc_members);

	debug_elog3(DEBUG2, "IsRunning: %u is not running", multi);

	return false;
}

/*
 * MultiXactIdSetOldestMember
 *		保存该事务可能成为成员的最旧的MultiXactId。
 *
 * 当一个给定的事务第一次进行可能需要MultiXactId的操作（元组锁、更新或删除）时，我们将OldestMemberMXactId设置为该事务。即使最终使用的是TransactionId而不是MultiXactId，我们也需要这样做，因为有可能另一个事务会将我们的XID添加到MultiXactId中。
 *
 * 要设置的值是下一个要分配的MultiXactId，因此这应该在进行任何可能的MultiXactId 操作之前调用。
 */
void MultiXactIdSetOldestMember(void)
{
	if (!MultiXactIdIsValid(OldestMemberMXactId[MyBackendId]))
	{
		MultiXactId fc_nextMXact;

		/*
		 * 你可能会认为我们在这里不需要获取锁，因为
		 * 获取和存储 TransactionIds 可能是原子的，但实际上我们需要：假设我们取到了 nextMXact，然后失去了 CPU 很长时间。其他人可能会推进 nextMXact，然后另一个人可能会计算出一个 OldestVisibleMXactId，这将位于我们将在控制权返回时存储的值之后。这将是错误的。
		 *
		 * 请注意，共享锁就足够了，因为它足以阻止其他人推进 nextMXact；并且其他人不会试图对我们的 OldestMember 条目进行写入，只能读取（我们假设存储它是原子的。）
		 */
		LWLockAcquire(MultiXactGenLock, LW_SHARED);

		/*
		 * 我们必须小心 nextMXact 进入环绕状态的可能性。
		 * 我们在这里不修复计数器本身，但我们必须确保在我们的数组条目中存储一个有效值。
		 */
		fc_nextMXact = MultiXactState->nextMXact;
		if (fc_nextMXact < FirstMultiXactId)
			fc_nextMXact = FirstMultiXactId;

		OldestMemberMXactId[MyBackendId] = fc_nextMXact;

		LWLockRelease(MultiXactGenLock);

		debug_elog4(DEBUG2, "MultiXact: setting OldestMember[%d] = %u",
					MyBackendId, nextMXact);
	}
}

/*
 * MultiXactIdSetOldestVisible
 *		保存该事务认为可能仍然活跃的最旧 MultiXactId。
 *
 * 我们在给定事务第一次检查任何 MultiXactId 时设置 OldestVisibleMXactId。一旦我们设置了这个，我们可以保证检查点不会截断在我们的 OldestVisibleMXactId 之后的 MultiXactId 的 SLRU 数据。
 *
 * 要设置的值是 nextMXact 和所有有效的每后端 OldestMemberMXactId[] 条目中的最旧者。由于我们所做的锁定，我们可以确定后续调用 MultiXactIdSetOldestMember 不会设置比我们在这里计算的 OldestMemberMXactId[] 条目更旧的值。因此，现在或将来没有活跃的事务可以是比我们在这里计算的 OldestVisibleMXactId 更旧的任何 MultiXactId 的成员。
 */
static void fc_MultiXactIdSetOldestVisible(void)
{
	if (!MultiXactIdIsValid(OldestVisibleMXactId[MyBackendId]))
	{
		MultiXactId fc_oldestMXact;
		int			fc_i;

		LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);

		/*
		 * 我们必须小心 nextMXact 进入环绕状态的可能性。
		 * 我们在这里不修复计数器本身，但我们必须确保在我们的数组条目中存储一个有效值。
		 */
		fc_oldestMXact = MultiXactState->nextMXact;
		if (fc_oldestMXact < FirstMultiXactId)
			fc_oldestMXact = FirstMultiXactId;

		for (fc_i = 1; fc_i <= MaxOldestSlot; fc_i++)
		{
			MultiXactId fc_thisoldest = OldestMemberMXactId[fc_i];

			if (MultiXactIdIsValid(fc_thisoldest) &&
				MultiXactIdPrecedes(fc_thisoldest, fc_oldestMXact))
				fc_oldestMXact = fc_thisoldest;
		}

		OldestVisibleMXactId[MyBackendId] = fc_oldestMXact;

		LWLockRelease(MultiXactGenLock);

		debug_elog4(DEBUG2, "MultiXact: setting OldestVisible[%d] = %u",
					MyBackendId, oldestMXact);
	}
}

/*
 * ReadNextMultiXactId
 *		返回下一个要分配的 MultiXactId，但不分配它
 */
MultiXactId ReadNextMultiXactId(void)
{
	MultiXactId fc_mxid;

	/* XXX 我们可以推测在没有锁的情况下做到这一点。 */
	LWLockAcquire(MultiXactGenLock, LW_SHARED);
	fc_mxid = MultiXactState->nextMXact;
	LWLockRelease(MultiXactGenLock);

	if (fc_mxid < FirstMultiXactId)
		fc_mxid = FirstMultiXactId;

	return fc_mxid;
}

/*
 * ReadMultiXactIdRange
 *		获取可能仍被引用的关系的 ID 范围。
 */
void ReadMultiXactIdRange(MultiXactId *fc_oldest, MultiXactId *fc_next)
{
	LWLockAcquire(MultiXactGenLock, LW_SHARED);
	*fc_oldest = MultiXactState->oldestMultiXactId;
	*fc_next = MultiXactState->nextMXact;
	LWLockRelease(MultiXactGenLock);

	if (*fc_oldest < FirstMultiXactId)
		*fc_oldest = FirstMultiXactId;
	if (*fc_next < FirstMultiXactId)
		*fc_next = FirstMultiXactId;
}


/*
 * MultiXactIdCreateFromMembers
 *		从指定的成员集合中创建一个新的 MultiXactId
 *
 * 为新的 MultiXactId 创建 XLOG、SLRU 和缓存条目，记录给定的 TransactionIds 作为成员。返回新创建的 MultiXactId。
 *
 * 注意：传递的 members[] 数组将就地排序。
 */
MultiXactId MultiXactIdCreateFromMembers(int fc_nmembers, MultiXactMember *fc_members)
{
	MultiXactId fc_multi;
	MultiXactOffset fc_offset;
	xl_multixact_create fc_xlrec;

	debug_elog3(DEBUG2, "Create: %s",
				mxid_to_string(InvalidMultiXactId, nmembers, members));

	/*
	 * 查看我们的缓存中是否已经存在相同的成员集合；如果存在，直接
	 * 重新使用那个 MultiXactId。（注意：看起来在我们的缓存中查找是不够的，
	 * 我们应该去磁盘上查看是否已经存在重复定义。但由于我们只创建包含
	 * 自己 XID 的 MultiXacts，在大多数情况下，这些 MultiXacts 实际上是
	 * 由我们创建的，因此会在我们的缓存中。确实存在一些边缘情况，比如
	 * 其他人将我们添加到一个 MultiXact 中而我们并不知道，但这并不值得
	 * 检查。）
	 */
	fc_multi = fc_mXactCacheGetBySet(fc_nmembers, fc_members);
	if (MultiXactIdIsValid(fc_multi))
	{
		debug_elog2(DEBUG2, "Create: in cache!");
		return fc_multi;
	}

	/* 验证给定成员中是否只有一个更新的 Xid。 */
	{
		int			fc_i;
		bool		fc_has_update = false;

		for (fc_i = 0; fc_i < fc_nmembers; fc_i++)
		{
			if (ISUPDATE_from_mxstatus(fc_members[fc_i].status))
			{
				if (fc_has_update)
					elog(ERROR, "new multixact has more than one updating member: %s",
						 mxid_to_string(InvalidMultiXactId, fc_nmembers, fc_members));
				fc_has_update = true;
			}
		}
	}

	/*
	 * 分配 MXID 和要使用的偏移量范围，并确保在 OFFSETs 和 MEMBERs
	 * 文件中有空间。注意：此例程会调用 START_CRIT_SECTION()。
	 *
	 * 注意：与 MultiXactIdCreate 和 MultiXactIdExpand 不同，我们不会在此
	 * 检查是否已经调用了 MultiXactIdSetOldestMember。这是因为这个例程
	 * 在某些地方用来创建当前后端不是成员的新 MultiXactIds，特别是在
	 * vacuum 中冻结多项时。在 vacuum 过程中，特别是不应该保持 OldestMulti
	 * 设置，以防它运行时间过长。
	 */
	fc_multi = fc_GetNewMultiXactId(fc_nmembers, &fc_offset);

	/* 进行一个 XLOG 条目，描述新的 MXID。 */
	fc_xlrec.mid = fc_multi;
	fc_xlrec.moff = fc_offset;
	fc_xlrec.nmembers = fc_nmembers;

	/*
	 * XXX 注意：在 MultiXactMember 中有很多填充空间。我们可以找到一个
	 * 更紧凑的 Xlog 记录表示形式——或许将所有状态标志放在一个 XLogRecData
	 * 中，然后将所有 xids 放在另一个中？但不清楚这是否值得费心。
	 */
	XLogBeginInsert();
	XLogRegisterData((char *) (&fc_xlrec), SizeOfMultiXactCreate);
	XLogRegisterData((char *) fc_members, fc_nmembers * sizeof(MultiXactMember));

	(void) XLogInsert(RM_MULTIXACT_ID, XLOG_MULTIXACT_CREATE_ID);

	/* 现在将信息输入到 OFFSETs 和 MEMBERs 日志中 */
	fc_RecordNewMultiXact(fc_multi, fc_offset, fc_nmembers, fc_members);

	/* 完成临界区 */
	END_CRIT_SECTION();

	/* 也将新的 MultiXactId 存储在本地缓存中 */
	fc_mXactCachePut(fc_multi, fc_nmembers, fc_members);

	debug_elog2(DEBUG2, "Create: all done");

	return fc_multi;
}

/*
 * RecordNewMultiXact
 *		将新多重交易的信息写入偏移量和成员文件
 *
 * 这段代码从 MultiXactIdCreateFromMembers 中拆分出来，以便 xlog 回放
 * 可以使用它。
 */
static void fc_RecordNewMultiXact(MultiXactId fc_multi, MultiXactOffset fc_offset,
				   int fc_nmembers, MultiXactMember *fc_members)
{
	int			fc_pageno;
	int			fc_prev_pageno;
	int			fc_entryno;
	int			fc_slotno;
	MultiXactOffset *fc_offptr;
	int			fc_i;

	LWLockAcquire(MultiXactOffsetSLRULock, LW_EXCLUSIVE);

	fc_pageno = MultiXactIdToOffsetPage(fc_multi);
	fc_entryno = MultiXactIdToOffsetEntry(fc_multi);

	/*
	 * 注意：我们将 MultiXactId 传递给 SimpleLruReadPage，作为 "transaction"
	 * 以便在发生任何 I/O 错误时进行报告。这有点牵强，但由于错误总是会
	 * 给出完整路径名，应该很清楚 MultiXactId 真的涉及到。也许将来我们
	 * 会费心将 slru.c 的错误报告代码进行通用化。
	 */
	fc_slotno = SimpleLruReadPage(MultiXactOffsetCtl, fc_pageno, true, fc_multi);
	fc_offptr = (MultiXactOffset *) MultiXactOffsetCtl->shared->page_buffer[fc_slotno];
	fc_offptr += fc_entryno;

	*fc_offptr = fc_offset;

	MultiXactOffsetCtl->shared->page_dirty[fc_slotno] = true;

	/* 交换我们的锁 */
	LWLockRelease(MultiXactOffsetSLRULock);

	LWLockAcquire(MultiXactMemberSLRULock, LW_EXCLUSIVE);

	fc_prev_pageno = -1;

	for (fc_i = 0; fc_i < fc_nmembers; fc_i++, fc_offset++)
	{
		TransactionId *fc_memberptr;
		uint32	   *fc_flagsptr;
		uint32		fc_flagsval;
		int			fc_bshift;
		int			fc_flagsoff;
		int			fc_memberoff;

		Assert(fc_members[fc_i].status <= MultiXactStatusUpdate);

		fc_pageno = MXOffsetToMemberPage(fc_offset);
		fc_memberoff = MXOffsetToMemberOffset(fc_offset);
		fc_flagsoff = MXOffsetToFlagsOffset(fc_offset);
		fc_bshift = MXOffsetToFlagsBitShift(fc_offset);

		if (fc_pageno != fc_prev_pageno)
		{
			fc_slotno = SimpleLruReadPage(MultiXactMemberCtl, fc_pageno, true, fc_multi);
			fc_prev_pageno = fc_pageno;
		}

		fc_memberptr = (TransactionId *)
			(MultiXactMemberCtl->shared->page_buffer[fc_slotno] + fc_memberoff);

		*fc_memberptr = fc_members[fc_i].xid;

		fc_flagsptr = (uint32 *)
			(MultiXactMemberCtl->shared->page_buffer[fc_slotno] + fc_flagsoff);

		fc_flagsval = *fc_flagsptr;
		fc_flagsval &= ~(((1 << MXACT_MEMBER_BITS_PER_XACT) - 1) << fc_bshift);
		fc_flagsval |= (fc_members[fc_i].status << fc_bshift);
		*fc_flagsptr = fc_flagsval;

		MultiXactMemberCtl->shared->page_dirty[fc_slotno] = true;
	}

	LWLockRelease(MultiXactMemberSLRULock);
}

/*
 * GetNewMultiXactId
 *		获取下一个 MultiXactId。
 *
 * 同时，为“成员”区域保留所需的空间。保留空间的起始偏移量将返回在 *offset 中。
 *
 * 这可能会生成用于扩展偏移量和/或成员文件的 XLOG 记录。不幸的是，我们必须在持有 MultiXactGenLock 的同时进行，以避免竞争条件——归零页面的 XLOG 记录必须在任何后台可能尝试在该页面中存储数据之前出现！
 *
 * 我们在推进共享计数器之前开始一个关键区域。调用者必须在写入 SLRU 数据后结束关键区域。
 */
static MultiXactId
fc_GetNewMultiXactId(int fc_nmembers, MultiXactOffset *fc_offset)
{
	MultiXactId fc_result;
	MultiXactOffset fc_nextOffset;

	debug_elog3(DEBUG2, "GetNew: for %d xids", nmembers);

	/* 安全检查，我们在 HS 备用中永远不应该走这么远 */
	if (RecoveryInProgress())
		elog(ERROR, "cannot assign MultiXactIds during recovery");

	LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);

	/* 处理下一个 MXact 计数器的环绕 */
	if (MultiXactState->nextMXact < FirstMultiXactId)
		MultiXactState->nextMXact = FirstMultiXactId;

	/* 分配 MXID */
	fc_result = MultiXactState->nextMXact;

	/*----------
	 * 检查是否可以安全地分配另一个 MultiXactId。这保护
	 * 免于因 multixact 环绕而导致灾难性数据丢失。基本
	 * 规则是：
	 *
	 * 如果我们超过了 multiVacLimit 或成员存储空间的安全阈值， 
	 * 或者我们不知道成员存储的安全阈值是什么， 
	 * 开始尝试强制执行自动清理周期。
	 * 如果我们超过了 multiWarnLimit，开始发出警告。
	 * 如果我们超过了 multiStopLimit，拒绝创建新的 MultiXactIds。
	 *
	 * 请注意，这些几乎与获取新事务 ID 时的保护措施相同。
	 *----------
	 */
	if (!MultiXactIdPrecedes(fc_result, MultiXactState->multiVacLimit))
	{
		/*
		 * 出于安全考虑，我们在发送信号、警告等时释放 MultiXactGenLock。
		 * 这并不是因为我们关心在这种情况下保持并发，而是为了避免在进行 get_database_name() 时出现死锁的任何可能性。首先，
		 * 复制我们在此路径中所需的所有共享值。
		 */
		MultiXactId fc_multiWarnLimit = MultiXactState->multiWarnLimit;
		MultiXactId fc_multiStopLimit = MultiXactState->multiStopLimit;
		MultiXactId fc_multiWrapLimit = MultiXactState->multiWrapLimit;
		Oid			fc_oldest_datoid = MultiXactState->oldestMultiXactDB;

		LWLockRelease(MultiXactGenLock);

		if (IsUnderPostmaster &&
			!MultiXactIdPrecedes(fc_result, fc_multiStopLimit))
		{
			char	   *fc_oldest_datname = get_database_name(fc_oldest_datoid);

			/*
			 * 立即启动自动清理，因为我们已经处于错误状态。
			 */
			SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_LAUNCHER);

			/* 即使该数据库已经消失也要抱怨 */
			if (fc_oldest_datname)
				ereport(ERROR,
						(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
						 errmsg("database is not accepting commands that generate new MultiXactIds to avoid wraparound data loss in database \"%s\"",
								fc_oldest_datname),
						 errhint("Execute a database-wide VACUUM in that database.\n"
								 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
			else
				ereport(ERROR,
						(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
						 errmsg("database is not accepting commands that generate new MultiXactIds to avoid wraparound data loss in database with OID %u",
								fc_oldest_datoid),
						 errhint("Execute a database-wide VACUUM in that database.\n"
								 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
		}

		/*
		 * 为了避免使主进程被信号淹没，我们每生成 64K multis 仅发出一次自动清理请求。
		 * 这仍然提供了充足的机会，避免我们陷入真正的麻烦。
		 */
		if (IsUnderPostmaster && (fc_result % 65536) == 0)
			SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_LAUNCHER);

		if (!MultiXactIdPrecedes(fc_result, fc_multiWarnLimit))
		{
			char	   *fc_oldest_datname = get_database_name(fc_oldest_datoid);

			/* 即使该数据库已经消失也要抱怨 */
			if (fc_oldest_datname)
				ereport(WARNING,
						(errmsg_plural("database \"%s\" must be vacuumed before %u more MultiXactId is used",
									   "database \"%s\" must be vacuumed before %u more MultiXactIds are used",
									   fc_multiWrapLimit - fc_result,
									   fc_oldest_datname,
									   fc_multiWrapLimit - fc_result),
						 errhint("Execute a database-wide VACUUM in that database.\n"
								 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
			else
				ereport(WARNING,
						(errmsg_plural("database with OID %u must be vacuumed before %u more MultiXactId is used",
									   "database with OID %u must be vacuumed before %u more MultiXactIds are used",
									   fc_multiWrapLimit - fc_result,
									   fc_oldest_datoid,
									   fc_multiWrapLimit - fc_result),
						 errhint("Execute a database-wide VACUUM in that database.\n"
								 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
		}

		/* 重新获取锁并重新开始 */
		LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
		fc_result = MultiXactState->nextMXact;
		if (fc_result < FirstMultiXactId)
			fc_result = FirstMultiXactId;
	}

	/* 确保文件中有 MXID 的空间。 */
	fc_ExtendMultiXactOffset(fc_result);

	/*
	 * 类似于上述内容，保留成员空间。此外，要小心不要
	 * 以零作为任何 multixact 的起始偏移量返回。有关动机，请参见 GetMultiXactIdMembers()。
	 */
	fc_nextOffset = MultiXactState->nextOffset;
	if (fc_nextOffset == 0)
	{
		*fc_offset = 1;
		fc_nmembers++;				/* 也分配成员槽 0 */
	}
	else
		*fc_offset = fc_nextOffset;

	/*----------
	 * 通过以下规则防止成员空间的溢出：
	 *
	 * 如果我们超过了 offsetStopLimit，拒绝生成更多的多重（multis）。
	 * 如果我们接近 offsetStopLimit，发出警告。
	 *
	 * 任意地，当我们距离 offsetStopLimit 还有 20 个段或更少时开始发出警告。
	 *
	 * 注意我们尚未更新共享状态，因此如果我们在此时失败，我们获取的 multixact ID 仍可以被下一个用户使用。
	 *
	 * 注意，在这里强制执行自动真空（autovacuum）运行是没有意义的：必须降低 multixact 冻结设置才能产生任何效果。
	 *----------
	 */
#define OFFSET_WARN_SEGMENTS	20
	if (MultiXactState->oldestOffsetKnown &&
		fc_MultiXactOffsetWouldWrap(MultiXactState->offsetStopLimit, fc_nextOffset,
								 fc_nmembers))
	{
		/* 查看相应的偏移量环绕（wraparound）情况中的注释 */
		SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_LAUNCHER);

		ereport(ERROR,
				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
				 errmsg("multixact \"members\" limit exceeded"),
				 errdetail_plural("This command would create a multixact with %u members, but the remaining space is only enough for %u member.",
								  "This command would create a multixact with %u members, but the remaining space is only enough for %u members.",
								  MultiXactState->offsetStopLimit - fc_nextOffset - 1,
								  fc_nmembers,
								  MultiXactState->offsetStopLimit - fc_nextOffset - 1),
				 errhint("Execute a database-wide VACUUM in database with OID %u with reduced vacuum_multixact_freeze_min_age and vacuum_multixact_freeze_table_age settings.",
						 MultiXactState->oldestMultiXactDB)));
	}

	/*
	 * 检查我们是否应该启动自动真空（autovacuum），以防止成员环绕（wraparound）。注意，我们使用的触发自动真空的窗口远大于警告限制。警告只是最后的手段——这与 GetNewTransactionId 的行为一致。
	 */
	if (!MultiXactState->oldestOffsetKnown ||
		(MultiXactState->nextOffset - MultiXactState->oldestOffset
		 > MULTIXACT_MEMBER_SAFE_THRESHOLD))
	{
		/*
		 * 为了避免向主进程（postmaster）发送过多信号，我们仅在跨越段边界时发出自动真空请求。根据默认编译设置，这大约是在 50k 个成员之后。这仍然在我们陷入真正麻烦之前提供了很多机会。
		 */
		if ((MXOffsetToMemberPage(fc_nextOffset) / SLRU_PAGES_PER_SEGMENT) !=
			(MXOffsetToMemberPage(fc_nextOffset + fc_nmembers) / SLRU_PAGES_PER_SEGMENT))
			SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_LAUNCHER);
	}

	if (MultiXactState->oldestOffsetKnown &&
		fc_MultiXactOffsetWouldWrap(MultiXactState->offsetStopLimit,
								 fc_nextOffset,
								 fc_nmembers + MULTIXACT_MEMBERS_PER_PAGE * SLRU_PAGES_PER_SEGMENT * OFFSET_WARN_SEGMENTS))
		ereport(WARNING,
				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
				 errmsg_plural("database with OID %u must be vacuumed before %d more multixact member is used",
							   "database with OID %u must be vacuumed before %d more multixact members are used",
							   MultiXactState->offsetStopLimit - fc_nextOffset + fc_nmembers,
							   MultiXactState->oldestMultiXactDB,
							   MultiXactState->offsetStopLimit - fc_nextOffset + fc_nmembers),
				 errhint("Execute a database-wide VACUUM in that database with reduced vacuum_multixact_freeze_min_age and vacuum_multixact_freeze_table_age settings.")));

	fc_ExtendMultiXactMember(fc_nextOffset, fc_nmembers);

	/*
	 * 从这里到调用者将数据写入刚预留的 SLRU 空间的关键部分；我们不想在部分写入的 MultiXact 结构中出现错误。（特别地，在前进 nextMXact 后未能写入我们的起始偏移量将有效地破坏先前的 MultiXact。）
	 */
	START_CRIT_SECTION();

	/*
	 * 前进计数器。与 GetNewTransactionId() 中一样，这必须在文件扩展成功之后才发生！
	 *
	 * 我们不关心这里的 MultiXactId 环绕；它将在下一次迭代中处理。但请注意，nextMXact 可能是 InvalidMultiXactId，或者是该例程退出后在段开始页上的第一个值，因此其他任何查看该变量的人必须准备处理这两种情况。同样，nextOffset 可能是零，但是我们不会将其作为下一个 multixact 的实际起始偏移量使用。
	 */
	(MultiXactState->nextMXact)++;

	MultiXactState->nextOffset += fc_nmembers;

	LWLockRelease(MultiXactGenLock);

	debug_elog4(DEBUG2, "GetNew: returning %u offset %u", result, *offset);
	return fc_result;
}

/*
 * 获取MultiXactId成员
 *		返回组成MultiXactId的MultiXactMembers集合
 *
 * 返回值是找到的成员数量，或者如果没有则返回-1，
 * 并且*members被设置为一个新分配的成员数组。当使用完毕时，
 * 需要调用者负责释放它。
 *
 * from_pgupgrade必须在仅当且仅当multixact
 * 仅对应于在9.2或更早版本中被锁定的元组的值，并且
 * 后来进行了pg_upgrade时传递为true（也就是说，infomask是
 * HEAP_LOCKED_UPGRADED）。在这种情况下，我们可以确定没有成员
 * 仍然可以运行，因此我们返回-1，就像对于没有任何进一步检查的
 * 空multixact一样。尝试解析这样的multixact将是错误的：要么
 * multixact在当前有效的multixact范围内，在这种情况下返回的结果
 * 将是伪造的，或者在那个范围之外，在这种情况下将引发错误。
 *
 * 在所有其他情况下，传入的multixact必须在已知的有效
 * 范围内，即大于或等于oldestMultiXactId，小于
 * nextMXact。否则，将引发错误。
 *
 * onlyLock必须设置为true，如果调用者确定给定的multi
 * 仅用于锁定元组；可以在不丧失正确性的情况下为false，
 * 但是传递true意味着我们可以快速返回而无需检查
 * 旧的更新。
 */
int GetMultiXactIdMembers(MultiXactId fc_multi, MultiXactMember **fc_members,
					  bool fc_from_pgupgrade, bool fc_onlyLock)
{
	int			fc_pageno;
	int			fc_prev_pageno;
	int			fc_entryno;
	int			fc_slotno;
	MultiXactOffset *fc_offptr;
	MultiXactOffset fc_offset;
	int			fc_length;
	int			fc_truelength;
	int			fc_i;
	MultiXactId fc_oldestMXact;
	MultiXactId fc_nextMXact;
	MultiXactId fc_tmpMXact;
	MultiXactOffset fc_nextOffset;
	MultiXactMember *fc_ptr;

	debug_elog3(DEBUG2, "GetMembers: asked for %u", multi);

	if (!MultiXactIdIsValid(fc_multi) || fc_from_pgupgrade)
	{
		*fc_members = NULL;
		return -1;
	}

	/* 检查MultiXactId是否在本地缓存中 */
	fc_length = fc_mXactCacheGetById(fc_multi, fc_members);
	if (fc_length >= 0)
	{
		debug_elog3(DEBUG2, "GetMembers: found %s in the cache",
					mxid_to_string(multi, length, *members));
		return fc_length;
	}

	/* 如果我们还没有设置OldestVisibleMXactId[]条目，则设置它 */
	fc_MultiXactIdSetOldestVisible();

	/*
	 * 如果我们知道multi仅用于锁定而不用于更新，那么
	 * 我们可以跳过检查该值是否比我们最旧的可见
	 * multi更旧。它不可能仍在运行中。
	 */
	if (fc_onlyLock &&
		MultiXactIdPrecedes(fc_multi, OldestVisibleMXactId[MyBackendId]))
	{
		debug_elog2(DEBUG2, "GetMembers: a locker-only multi is too old");
		*fc_members = NULL;
		return -1;
	}

	/*
	 * 我们在求助于SLRU区域之前检查MultiXact的已知限制。
	 *
	 * 一个比MultiXactState->oldestMultiXactId更旧的ID
	 * 绝对没有可能有用；它已经被删除，或者将很快
	 * 被截断删除。如果传入一个，将引发错误。
	 *
	 * 此外，一个ID >= nextMXact不应该在这里出现；如果出现，
	 * 则意味着发生了未检测到的ID环绕。这将引发一个严重的
	 * 错误。
	 *
	 * 由于我们不修改任何全局状态，这里需要共享锁就足够。
	 * 仅获取足够长的时间来抓取当前计数器值。我们可能
	 * 需要nextMXact和nextOffset；见下文。
	 */
	LWLockAcquire(MultiXactGenLock, LW_SHARED);

	fc_oldestMXact = MultiXactState->oldestMultiXactId;
	fc_nextMXact = MultiXactState->nextMXact;
	fc_nextOffset = MultiXactState->nextOffset;

	LWLockRelease(MultiXactGenLock);

	if (MultiXactIdPrecedes(fc_multi, fc_oldestMXact))
		ereport(ERROR,
				(errcode(ERRCODE_INTERNAL_ERROR),
				 errmsg("MultiXactId %u does no longer exist -- apparent wraparound",
						fc_multi)));

	if (!MultiXactIdPrecedes(fc_multi, fc_nextMXact))
		ereport(ERROR,
				(errcode(ERRCODE_INTERNAL_ERROR),
				 errmsg("MultiXactId %u has not been created yet -- apparent wraparound",
						fc_multi)));

	/*
	 * 查找我们需要开始读取 MultiXactMembers 的偏移量
	 * 和 multixact 中成员的数量。我们通过
	 * 该 multixact 的起始偏移量与下一个的差值来确定后者。
	 * 但是，有一些边缘情况需要注意：
	 *
	 * 1. 这个 multixact 可能是最新创建的，因此没有下一个可查看的 multixact。在这种情况下，我们刚才保存的 nextOffset 值就是正确的终点。
	 *
	 * 2. 下一个 multixact 可能仍在填写中：也就是说，另一个进程可能已经执行了 GetNewMultiXactId，但尚未为该 ID 写入偏移入口。在这种情况下，确保该 multixact 的偏移入口存在（因为 GetNewMultiXactId 在此之前不会释放 MultiXactGenLock），但内容为零（因为我们小心地将偏移页面预先填零）。由于 GetNewMultiXactId 永远不会将零作为 multixact 的起始偏移量，当我们读取到零作为下一个 multixact 的偏移量时，我们知道我们遇到了这种情况。我们稍微等待一下，然后再试一次。
	 *
	 * 3. 由于 GetNewMultiXactId 将偏移零递增到偏移一以处理情况 #2，所以在偏移回绕点附近存在歧义。如果我们看到下一个 multixact 的偏移量为一，是否表示我们的 multixact 的实际终点，还是在零处结束并随后递增？我们利用以下知识来处理这个问题：如果零号成员槽没有被填充，它将包含零，而零不是有效的事务 ID，因此不可能是 multixact 成员。因此，如果我们从成员数组中读取到零，只需忽略它。
	 *
	 * 这一切都很复杂，但这种混乱仅发生在不常见的边缘情况下，所以这似乎比在每次创建 multixact 时长时间持有 MultiXactGenLock 要好。
	 */
retry:
	LWLockAcquire(MultiXactOffsetSLRULock, LW_EXCLUSIVE);

	fc_pageno = MultiXactIdToOffsetPage(fc_multi);
	fc_entryno = MultiXactIdToOffsetEntry(fc_multi);

	fc_slotno = SimpleLruReadPage(MultiXactOffsetCtl, fc_pageno, true, fc_multi);
	fc_offptr = (MultiXactOffset *) MultiXactOffsetCtl->shared->page_buffer[fc_slotno];
	fc_offptr += fc_entryno;
	fc_offset = *fc_offptr;

	Assert(fc_offset != 0);

	/*
	 * 使用与 GetNewMultiXactId() 相同的递增规则，即，在需要之前不要
	 * 显式处理回绕。
	 */
	fc_tmpMXact = fc_multi + 1;

	if (fc_nextMXact == fc_tmpMXact)
	{
		/* 边缘情况 1：没有下一个 multixact */
		fc_length = fc_nextOffset - fc_offset;
	}
	else
	{
		MultiXactOffset fc_nextMXOffset;

		/* 如有需要，处理回绕 */
		if (fc_tmpMXact < FirstMultiXactId)
			fc_tmpMXact = FirstMultiXactId;

		fc_prev_pageno = fc_pageno;

		fc_pageno = MultiXactIdToOffsetPage(fc_tmpMXact);
		fc_entryno = MultiXactIdToOffsetEntry(fc_tmpMXact);

		if (fc_pageno != fc_prev_pageno)
			fc_slotno = SimpleLruReadPage(MultiXactOffsetCtl, fc_pageno, true, fc_tmpMXact);

		fc_offptr = (MultiXactOffset *) MultiXactOffsetCtl->shared->page_buffer[fc_slotno];
		fc_offptr += fc_entryno;
		fc_nextMXOffset = *fc_offptr;

		if (fc_nextMXOffset == 0)
		{
			/* 边缘情况 2：下一个 multixact 仍在填写中 */
			LWLockRelease(MultiXactOffsetSLRULock);
			CHECK_FOR_INTERRUPTS();
			pg_usleep(1000L);
			goto retry;
		}

		fc_length = fc_nextMXOffset - fc_offset;
	}

	LWLockRelease(MultiXactOffsetSLRULock);

	fc_ptr = (MultiXactMember *) palloc(fc_length * sizeof(MultiXactMember));

	/* 现在获取成员本身。 */
	LWLockAcquire(MultiXactMemberSLRULock, LW_EXCLUSIVE);

	fc_truelength = 0;
	fc_prev_pageno = -1;
	for (fc_i = 0; fc_i < fc_length; fc_i++, fc_offset++)
	{
		TransactionId *fc_xactptr;
		uint32	   *fc_flagsptr;
		int			fc_flagsoff;
		int			fc_bshift;
		int			fc_memberoff;

		fc_pageno = MXOffsetToMemberPage(fc_offset);
		fc_memberoff = MXOffsetToMemberOffset(fc_offset);

		if (fc_pageno != fc_prev_pageno)
		{
			fc_slotno = SimpleLruReadPage(MultiXactMemberCtl, fc_pageno, true, fc_multi);
			fc_prev_pageno = fc_pageno;
		}

		fc_xactptr = (TransactionId *)
			(MultiXactMemberCtl->shared->page_buffer[fc_slotno] + fc_memberoff);

		if (!TransactionIdIsValid(*fc_xactptr))
		{
			/* 边缘情况 3：我们必须查看未使用的槽零 */
			Assert(fc_offset == 0);
			continue;
		}

		fc_flagsoff = MXOffsetToFlagsOffset(fc_offset);
		fc_bshift = MXOffsetToFlagsBitShift(fc_offset);
		fc_flagsptr = (uint32 *) (MultiXactMemberCtl->shared->page_buffer[fc_slotno] + fc_flagsoff);

		fc_ptr[fc_truelength].xid = *fc_xactptr;
		fc_ptr[fc_truelength].status = (*fc_flagsptr >> fc_bshift) & MXACT_MEMBER_XACT_BITMASK;
		fc_truelength++;
	}

	LWLockRelease(MultiXactMemberSLRULock);

	/* 一个零成员的 multixid 不应发生 */
	Assert(fc_truelength > 0);

	/*
	 * 将结果复制到本地缓存中。
	 */
	fc_mXactCachePut(fc_multi, fc_truelength, fc_ptr);

	debug_elog3(DEBUG2, "GetMembers: no cache for %s",
				mxid_to_string(multi, truelength, ptr));
	*fc_members = fc_ptr;
	return fc_truelength;
}

/*
 * mxactMemberComparator
 *		qsort 比较函数用于 MultiXactMember
 *
 * 我们不能对 XIDs 使用回绕比较，因为这不符合
 * 三角不等式！任何旧的排序顺序都可以。
 */
static int fc_mxactMemberComparator(const void *fc_arg1, const void *fc_arg2)
{
	MultiXactMember fc_member1 = *(const MultiXactMember *) fc_arg1;
	MultiXactMember fc_member2 = *(const MultiXactMember *) fc_arg2;

	if (fc_member1.xid > fc_member2.xid)
		return 1;
	if (fc_member1.xid < fc_member2.xid)
		return -1;
	if (fc_member1.status > fc_member2.status)
		return 1;
	if (fc_member1.status < fc_member2.status)
		return -1;
	return 0;
}

/*
 * mXactCacheGetBySet
 *		返回来自缓存的 MultiXactId，基于构成它的
 *		TransactionIds 集合，或在没有匹配时返回 InvalidMultiXactId。
 *
 * 这在例如，当两个事务想要锁定一个巨大的
 * 表时是有帮助的。通过使用缓存，第二个事务将为大多数元组使用相同的 MultiXactId，
 * 从而保持 MultiXactId 使用率低（节省
 * I/O 和环绕问题）。
 *
 * 注意：传入的成员数组将就地排序。
 */
static MultiXactId
fc_mXactCacheGetBySet(int fc_nmembers, MultiXactMember *fc_members)
{
	dlist_iter	fc_iter;

	debug_elog3(DEBUG2, "CacheGet: looking for %s",
				mxid_to_string(InvalidMultiXactId, nmembers, members));

	/* 对数组进行排序，以便比较容易 */
	qsort(fc_members, fc_nmembers, sizeof(MultiXactMember), fc_mxactMemberComparator);

	dlist_foreach(fc_iter, &MXactCache)
	{
		mXactCacheEnt *fc_entry = dlist_container(mXactCacheEnt, node, fc_iter.cur);

		if (fc_entry->nmembers != fc_nmembers)
			continue;

		/*
		 * 我们假设缓存条目是排序的，并且
		 * "status" 中未使用的位被清零。
		 */
		if (memcmp(fc_members, fc_entry->members, fc_nmembers * sizeof(MultiXactMember)) == 0)
		{
			debug_elog3(DEBUG2, "CacheGet: found %u", entry->multi);
			dlist_move_head(&MXactCache, fc_iter.cur);
			return fc_entry->multi;
		}
	}

	debug_elog2(DEBUG2, "CacheGet: not found :-(");
	return InvalidMultiXactId;
}

/*
 * mXactCacheGetById
 *		返回给定 MultiXactId 的组成 MultiXactMember 集合
 *		如果存在的话，来自缓存。
 *
 * 如果成功，*xids 被设置为已分配的
 * MultiXactMember 集合的地址。返回值是成员的数量，或在失败时返回 -1。
 */
static int fc_mXactCacheGetById(MultiXactId fc_multi, MultiXactMember **fc_members)
{
	dlist_iter	fc_iter;

	debug_elog3(DEBUG2, "CacheGet: looking for %u", multi);

	dlist_foreach(fc_iter, &MXactCache)
	{
		mXactCacheEnt *fc_entry = dlist_container(mXactCacheEnt, node, fc_iter.cur);

		if (fc_entry->multi == fc_multi)
		{
			MultiXactMember *fc_ptr;
			Size		fc_size;

			fc_size = sizeof(MultiXactMember) * fc_entry->nmembers;
			fc_ptr = (MultiXactMember *) palloc(fc_size);

			memcpy(fc_ptr, fc_entry->members, fc_size);

			debug_elog3(DEBUG2, "CacheGet: found %s",
						mxid_to_string(multi,
									   entry->nmembers,
									   entry->members));

			/*
			 * 请注意，我们在不使用可修改迭代器的情况下修改列表。
			 * 这只有在我们立即退出迭代的情况下是可以接受的。
			 */
			dlist_move_head(&MXactCache, fc_iter.cur);

			*fc_members = fc_ptr;
			return fc_entry->nmembers;
		}
	}

	debug_elog2(DEBUG2, "CacheGet: not found");
	return -1;
}

/*
 * mXactCachePut
 *		将一个新的 MultiXactId 及其组成集合添加到本地缓存中。
 */
static void fc_mXactCachePut(MultiXactId fc_multi, int fc_nmembers, MultiXactMember *fc_members)
{
	mXactCacheEnt *fc_entry;

	debug_elog3(DEBUG2, "CachePut: storing %s",
				mxid_to_string(multi, nmembers, members));

	if (MXactContext == NULL)
	{
		/* 缓存的生存期仅与当前事务相同 */
		debug_elog2(DEBUG2, "CachePut: initializing memory context");
		MXactContext = AllocSetContextCreate(TopTransactionContext,
											 "MultiXact cache context",
											 ALLOCSET_SMALL_SIZES);
	}

	fc_entry = (mXactCacheEnt *)
		MemoryContextAlloc(MXactContext,
						   offsetof(mXactCacheEnt, members) +
						   fc_nmembers * sizeof(MultiXactMember));

	fc_entry->multi = fc_multi;
	fc_entry->nmembers = fc_nmembers;
	memcpy(fc_entry->members, fc_members, fc_nmembers * sizeof(MultiXactMember));

	/* mXactCacheGetBySet 假设条目是排序的，因此进行排序 */
	qsort(fc_entry->members, fc_nmembers, sizeof(MultiXactMember), fc_mxactMemberComparator);

	dlist_push_head(&MXactCache, &fc_entry->node);
	if (MXactCacheMembers++ >= MAX_CACHE_ENTRIES)
	{
		dlist_node *fc_node;
		mXactCacheEnt *fc_entry;

		fc_node = dlist_tail_node(&MXactCache);
		dlist_delete(fc_node);
		MXactCacheMembers--;

		fc_entry = dlist_container(mXactCacheEnt, node, fc_node);
		debug_elog3(DEBUG2, "CachePut: pruning cached multi %u",
					entry->multi);

		pfree(fc_entry);
	}
}

static char * fc_mxstatus_to_string(MultiXactStatus fc_status)
{
	switch (fc_status)
	{
		case MultiXactStatusForKeyShare:
			return "keysh";
		case MultiXactStatusForShare:
			return "sh";
		case MultiXactStatusForNoKeyUpdate:
			return "fornokeyupd";
		case MultiXactStatusForUpdate:
			return "forupd";
		case MultiXactStatusNoKeyUpdate:
			return "nokeyupd";
		case MultiXactStatusUpdate:
			return "upd";
		default:
			elog(ERROR, "unrecognized multixact status %d", fc_status);
			return "";
	}
}

char * mxid_to_string(MultiXactId fc_multi, int fc_nmembers, MultiXactMember *fc_members)
{
	static char *fc_str = NULL;
	StringInfoData fc_buf;
	int			fc_i;

	if (fc_str != NULL)
		pfree(fc_str);

	initStringInfo(&fc_buf);

	appendStringInfo(&fc_buf, "%u %d[%u (%s)", fc_multi, fc_nmembers, fc_members[0].xid,
					 fc_mxstatus_to_string(fc_members[0].status));

	for (fc_i = 1; fc_i < fc_nmembers; fc_i++)
		appendStringInfo(&fc_buf, ", %u (%s)", fc_members[fc_i].xid,
						 fc_mxstatus_to_string(fc_members[fc_i].status));

	appendStringInfoChar(&fc_buf, ']');
	fc_str = MemoryContextStrdup(TopMemoryContext, fc_buf.data);
	pfree(fc_buf.data);
	return fc_str;
}

/*
 * AtEOXact_MultiXact
 *		处理 MultiXact 的事务结束
 *
 * 这在顶级事务提交或中止时被调用（我们不在乎是哪种情况）。
 */
void AtEOXact_MultiXact(void)
{
	/*
	 * 重置我们 OldestMemberMXactId 和 OldestVisibleMXactId 值，这两个值
	 * 仅在事务内有效。
	 *
	 * 我们假设存储 MultiXactId 是原子性的，因此我们不需要
	 * 进行 MultiXactGenLock。
	 */
	OldestMemberMXactId[MyBackendId] = InvalidMultiXactId;
	OldestVisibleMXactId[MyBackendId] = InvalidMultiXactId;

	/*
	 * 丢弃本地 MultiXactId 缓存。由于 MXactContext 是作为
	 * TopTransactionContext 的子上下文创建的，因此我们不需要显式删除它。
	 */
	MXactContext = NULL;
	dlist_init(&MXactCache);
	MXactCacheMembers = 0;
}

/*
 * AtPrepare_MultiXact
 *		在 2PC 事务准备时保存 multixact 状态
 *
 * 在此阶段，我们仅将我们的 OldestMemberMXactId 值存储在二阶段
 * 状态文件中。
 */
void AtPrepare_MultiXact(void)
{
	MultiXactId fc_myOldestMember = OldestMemberMXactId[MyBackendId];

	if (MultiXactIdIsValid(fc_myOldestMember))
		RegisterTwoPhaseRecord(TWOPHASE_RM_MULTIXACT_ID, 0,
							   &fc_myOldestMember, sizeof(MultiXactId));
}

/*
 * PostPrepare_MultiXact
 *		在成功的 PREPARE TRANSACTION 后清理
 */
void PostPrepare_MultiXact(TransactionId fc_xid)
{
	MultiXactId fc_myOldestMember;

	/*
	 * 将我们的 OldestMemberMXactId 值转移到为
	 * 准备好的事务保留的槽中。
	 */
	fc_myOldestMember = OldestMemberMXactId[MyBackendId];
	if (MultiXactIdIsValid(fc_myOldestMember))
	{
		BackendId	fc_dummyBackendId = TwoPhaseGetDummyBackendId(fc_xid, false);

		/*
		 * 尽管存储 MultiXactId 是原子性的，但仍需获取锁以确保
		 * 其他事务能够看到这两个更改，而不仅仅是当前后端的槽重置。
		 * 使用一个易失性指针可能就足够，但这
		 * 并不是热点。
		 */
		LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);

		OldestMemberMXactId[fc_dummyBackendId] = fc_myOldestMember;
		OldestMemberMXactId[MyBackendId] = InvalidMultiXactId;

		LWLockRelease(MultiXactGenLock);
	}

	/*
	 * 我们不需要转移 OldestVisibleMXactId 值，因为
	 * 事务在准备好后将不再查看任何其他 multixacts。
	 *
	 * 我们假设存储 MultiXactId 是原子性的，因此我们不需要
	 * 进行 MultiXactGenLock。
	 */
	OldestVisibleMXactId[MyBackendId] = InvalidMultiXactId;

	/*
	 * 像在 AtEOXact_MultiXact 中一样丢弃本地 MultiXactId 缓存。
	 */
	MXactContext = NULL;
	dlist_init(&MXactCache);
	MXactCacheMembers = 0;
}

/*
 * multixact_twophase_recover
 *		在启动时恢复准备事务的状态
 */
void multixact_twophase_recover(TransactionId fc_xid, uint16 fc_info,
						   void *fc_recdata, uint32 fc_len)
{
	BackendId	fc_dummyBackendId = TwoPhaseGetDummyBackendId(fc_xid, false);
	MultiXactId fc_oldestMember;

	/*
	 * 从状态文件记录中获取最旧的成员XID，并将其设置在
	 * 为该准备事务保留的OldestMemberMXactId槽中。
	 */
	Assert(fc_len == sizeof(MultiXactId));
	fc_oldestMember = *((MultiXactId *) fc_recdata);

	OldestMemberMXactId[fc_dummyBackendId] = fc_oldestMember;
}

/*
 * multixact_twophase_postcommit
 *		类似于AtEOXact_MultiXact，但用于COMMIT PREPARED
 */
void multixact_twophase_postcommit(TransactionId fc_xid, uint16 fc_info,
							  void *fc_recdata, uint32 fc_len)
{
	BackendId	fc_dummyBackendId = TwoPhaseGetDummyBackendId(fc_xid, true);

	Assert(fc_len == sizeof(MultiXactId));

	OldestMemberMXactId[fc_dummyBackendId] = InvalidMultiXactId;
}

/*
 * multixact_twophase_postabort
 *		这实际上与COMMIT情况相同。
 */
void multixact_twophase_postabort(TransactionId fc_xid, uint16 fc_info,
							 void *fc_recdata, uint32 fc_len)
{
	multixact_twophase_postcommit(fc_xid, fc_info, fc_recdata, fc_len);
}

/*
 * MultiXact的共享内存初始化。我们使用两个SLRU区域，
 * 因此双倍内存。此外，为共享的MultiXactState
 * 结构和每个后端的MultiXactId数组（两个）保留空间。
 */
Size MultiXactShmemSize(void)
{
	Size		fc_size;

	/* 每个后端XactIds[]条目需要2*MaxOldestSlot + 1 */
#define SHARED_MULTIXACT_STATE_SIZE \
	add_size(offsetof(MultiXactStateData, perBackendXactIds) + sizeof(MultiXactId), \
			 mul_size(sizeof(MultiXactId) * 2, MaxOldestSlot))

	fc_size = SHARED_MULTIXACT_STATE_SIZE;
	fc_size = add_size(fc_size, SimpleLruShmemSize(NUM_MULTIXACTOFFSET_BUFFERS, 0));
	fc_size = add_size(fc_size, SimpleLruShmemSize(NUM_MULTIXACTMEMBER_BUFFERS, 0));

	return fc_size;
}

void MultiXactShmemInit(void)
{
	bool		fc_found;

	debug_elog2(DEBUG2, "Shared Memory Init for MultiXact");

	MultiXactOffsetCtl->PagePrecedes = fc_MultiXactOffsetPagePrecedes;
	MultiXactMemberCtl->PagePrecedes = fc_MultiXactMemberPagePrecedes;

	SimpleLruInit(MultiXactOffsetCtl,
				  "MultiXactOffset", NUM_MULTIXACTOFFSET_BUFFERS, 0,
				  MultiXactOffsetSLRULock, "pg_multixact/offsets",
				  LWTRANCHE_MULTIXACTOFFSET_BUFFER,
				  SYNC_HANDLER_MULTIXACT_OFFSET);
	SlruPagePrecedesUnitTests(MultiXactOffsetCtl, MULTIXACT_OFFSETS_PER_PAGE);
	SimpleLruInit(MultiXactMemberCtl,
				  "MultiXactMember", NUM_MULTIXACTMEMBER_BUFFERS, 0,
				  MultiXactMemberSLRULock, "pg_multixact/members",
				  LWTRANCHE_MULTIXACTMEMBER_BUFFER,
				  SYNC_HANDLER_MULTIXACT_MEMBER);
	/* 不调用SimpleLruTruncate()或满足单元测试的标准 */

	/* 初始化我们的共享状态结构 */
	MultiXactState = ShmemInitStruct("Shared MultiXact State",
									 SHARED_MULTIXACT_STATE_SIZE,
									 &fc_found);
	if (!IsUnderPostmaster)
	{
		Assert(!fc_found);

		/* 确保将每个后端状态清零 */
		MemSet(MultiXactState, 0, SHARED_MULTIXACT_STATE_SIZE);
	}
	else
		Assert(fc_found);

	/*
	 * 设置数组指针。注意，perBackendXactIds[0]是浪费空间
	 * 因为我们只在每个数组中使用索引1..MaxOldestSlot。
	 */
	OldestMemberMXactId = MultiXactState->perBackendXactIds;
	OldestVisibleMXactId = OldestMemberMXactId + MaxOldestSlot;
}

/*
 * 该函数必须在系统安装时调用一次。它创建初始的
 * MultiXact段。（假设MultiXacts目录已由initdb创建，
 * 并且必须已调用MultiXactShmemInit。）
 */
void BootStrapMultiXact(void)
{
	int			fc_slotno;

	LWLockAcquire(MultiXactOffsetSLRULock, LW_EXCLUSIVE);

	/* 创建并清零偏移日志的第一页 */
	fc_slotno = fc_ZeroMultiXactOffsetPage(0, false);

	/* 确保它被写出 */
	SimpleLruWritePage(MultiXactOffsetCtl, fc_slotno);
	Assert(!MultiXactOffsetCtl->shared->page_dirty[fc_slotno]);

	LWLockRelease(MultiXactOffsetSLRULock);

	LWLockAcquire(MultiXactMemberSLRULock, LW_EXCLUSIVE);

	/* 创建并清零成员日志的第一页 */
	fc_slotno = fc_ZeroMultiXactMemberPage(0, false);

	/* 确保它被写出 */
	SimpleLruWritePage(MultiXactMemberCtl, fc_slotno);
	Assert(!MultiXactMemberCtl->shared->page_dirty[fc_slotno]);

	LWLockRelease(MultiXactMemberSLRULock);
}

/*
 * 初始化（或重新初始化）MultiXactOffset的一页为零。
 * 如果writeXlog为真，还将发出一条XLOG记录，说明我们这样做了。
 *
 * 该页实际上并没有写入，只是在共享内存中设置。
 * 新页的槽号将被返回。
 *
 * 入口时必须持有控制锁，并将在退出时保持。
 */
static int fc_ZeroMultiXactOffsetPage(int fc_pageno, bool fc_writeXlog)
{
	int			fc_slotno;

	fc_slotno = SimpleLruZeroPage(MultiXactOffsetCtl, fc_pageno);

	if (fc_writeXlog)
		fc_WriteMZeroPageXlogRec(fc_pageno, XLOG_MULTIXACT_ZERO_OFF_PAGE);

	return fc_slotno;
}

/*
 * 同样，适用于MultiXactMember
 */
static int fc_ZeroMultiXactMemberPage(int fc_pageno, bool fc_writeXlog)
{
	int			fc_slotno;

	fc_slotno = SimpleLruZeroPage(MultiXactMemberCtl, fc_pageno);

	if (fc_writeXlog)
		fc_WriteMZeroPageXlogRec(fc_pageno, XLOG_MULTIXACT_ZERO_MEM_PAGE);

	return fc_slotno;
}

/*
 * MaybeExtendOffsetSlru
 *		如有必要，扩展偏移SLRU区域
 *
 * 从<= 9.2的二进制升级后，pg_multixact/offsets SLRU区域可能
 * 包含短于必要的文件；如果旧安装使用了超过第一页的多重事务（无法
 * 复制文件，因为磁盘上的表示不同），则会发生这种情况。pg_upgrade将
 * 更新pg_control，将下一个偏移值设置为该位置，以便
 * 被标记为由这样的MultiXacts锁定的元组将在不查阅
 * multixact的情况下被视为可见。然而，尝试创建和使用新的
 * MultiXactId将导致错误，因为新值将存在的页面并不存在。
 * 该例程负责创建这些页面。
 */
static void fc_MaybeExtendOffsetSlru(void)
{
	int			fc_pageno;

	fc_pageno = MultiXactIdToOffsetPage(MultiXactState->nextMXact);

	LWLockAcquire(MultiXactOffsetSLRULock, LW_EXCLUSIVE);

	if (!SimpleLruDoesPhysicalPageExist(MultiXactOffsetCtl, fc_pageno))
	{
		int			fc_slotno;

		/*
		 * 对我们来说幸运的是，SimpleLruWritePage已经准备好处理
		 * 创建新段文件，即使我们正在写入的页面不是
		 * 该段中的第一个，因此这足够了。
		 */
		fc_slotno = fc_ZeroMultiXactOffsetPage(fc_pageno, false);
		SimpleLruWritePage(MultiXactOffsetCtl, fc_slotno);
	}

	LWLockRelease(MultiXactOffsetSLRULock);
}

/*
 * 这必须在postmaster或独立后端启动期间被调用一次。
 *
 * StartupXLOG已经通过调用MultiXactSetNextMXact和/或MultiXactAdvanceNextMXact建立了nextMXact/nextOffset，以及来自pg_control和/或MultiXactAdvanceOldest的oldestMulti信息，但我们尚未重放WAL。
 */
void StartupMultiXact(void)
{
	MultiXactId fc_multi = MultiXactState->nextMXact;
	MultiXactOffset fc_offset = MultiXactState->nextOffset;
	int			fc_pageno;

	/*
	 * 初始化offset对最新页面编号的想法。
	 */
	fc_pageno = MultiXactIdToOffsetPage(fc_multi);
	MultiXactOffsetCtl->shared->latest_page_number = fc_pageno;

	/*
	 * 初始化成员对最新页面编号的想法。
	 */
	fc_pageno = MXOffsetToMemberPage(fc_offset);
	MultiXactMemberCtl->shared->latest_page_number = fc_pageno;
}

/*
 * 在启动/恢复结束时，必须调用一次此函数。
 */
void TrimMultiXact(void)
{
	MultiXactId fc_nextMXact;
	MultiXactOffset fc_offset;
	MultiXactId fc_oldestMXact;
	Oid			fc_oldestMXactDB;
	int			fc_pageno;
	int			fc_entryno;
	int			fc_flagsoff;

	LWLockAcquire(MultiXactGenLock, LW_SHARED);
	fc_nextMXact = MultiXactState->nextMXact;
	fc_offset = MultiXactState->nextOffset;
	fc_oldestMXact = MultiXactState->oldestMultiXactId;
	fc_oldestMXactDB = MultiXactState->oldestMultiXactDB;
	LWLockRelease(MultiXactGenLock);

	/* 清理offset状态 */
	LWLockAcquire(MultiXactOffsetSLRULock, LW_EXCLUSIVE);

	/*
	 * (重新)初始化我们对offset的最新页面编号的想法。
	 */
	fc_pageno = MultiXactIdToOffsetPage(fc_nextMXact);
	MultiXactOffsetCtl->shared->latest_page_number = fc_pageno;

	/*
	 * 将当前offset页面的其余部分清零。有关背景信息，请参见
	 * TrimCLOG()中的说明。与CLOG不同，一些WAL记录覆盖每一个
	 * pg_multixact SLRU变更。此外，与CLOG不同，我们忽略WAL
	 * 规则“在数据之前写入xlog”，因此nextMXact的后继可能携带过时的、
	 * 非零的offset值。将这些值清零，以使GetMultiXactIdMembers()的
	 * 第2种情况正常运行。
	 */
	fc_entryno = MultiXactIdToOffsetEntry(fc_nextMXact);
	if (fc_entryno != 0)
	{
		int			fc_slotno;
		MultiXactOffset *fc_offptr;

		fc_slotno = SimpleLruReadPage(MultiXactOffsetCtl, fc_pageno, true, fc_nextMXact);
		fc_offptr = (MultiXactOffset *) MultiXactOffsetCtl->shared->page_buffer[fc_slotno];
		fc_offptr += fc_entryno;

		MemSet(fc_offptr, 0, BLCKSZ - (fc_entryno * sizeof(MultiXactOffset)));

		MultiXactOffsetCtl->shared->page_dirty[fc_slotno] = true;
	}

	LWLockRelease(MultiXactOffsetSLRULock);

	/* 对成员也做同样处理 */
	LWLockAcquire(MultiXactMemberSLRULock, LW_EXCLUSIVE);

	/*
	 * (重新)初始化我们对成员的最新页面编号的想法。
	 */
	fc_pageno = MXOffsetToMemberPage(fc_offset);
	MultiXactMemberCtl->shared->latest_page_number = fc_pageno;

	/*
	 * 将当前成员页面的其余部分清零。有关动机，请参见
	 * TrimCLOG()中的说明。
	 */
	fc_flagsoff = MXOffsetToFlagsOffset(fc_offset);
	if (fc_flagsoff != 0)
	{
		int			fc_slotno;
		TransactionId *fc_xidptr;
		int			fc_memberoff;

		fc_memberoff = MXOffsetToMemberOffset(fc_offset);
		fc_slotno = SimpleLruReadPage(MultiXactMemberCtl, fc_pageno, true, fc_offset);
		fc_xidptr = (TransactionId *)
			(MultiXactMemberCtl->shared->page_buffer[fc_slotno] + fc_memberoff);

		MemSet(fc_xidptr, 0, BLCKSZ - fc_memberoff);

		/*
		 * 注意：我们不需要将当前组其余成员的标志位清零，
		 * 因为它们在写入之前总是被重置。
		 */

		MultiXactMemberCtl->shared->page_dirty[fc_slotno] = true;
	}

	LWLockRelease(MultiXactMemberSLRULock);

	/* 发出我们正式启动的信号 */
	LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
	MultiXactState->finishedStartup = true;
	LWLockRelease(MultiXactGenLock);

	/* 现在计算下一个成员的环绕距离。 */
	SetMultiXactIdLimit(fc_oldestMXact, fc_oldestMXactDB, true);
}

/*
 * 获取要保存在检查点记录中的MultiXact数据
 */
void MultiXactGetCheckptMulti(bool fc_is_shutdown,
						 MultiXactId *fc_nextMulti,
						 MultiXactOffset *fc_nextMultiOffset,
						 MultiXactId *fc_oldestMulti,
						 Oid *fc_oldestMultiDB)
{
	LWLockAcquire(MultiXactGenLock, LW_SHARED);
	*fc_nextMulti = MultiXactState->nextMXact;
	*fc_nextMultiOffset = MultiXactState->nextOffset;
	*fc_oldestMulti = MultiXactState->oldestMultiXactId;
	*fc_oldestMultiDB = MultiXactState->oldestMultiXactDB;
	LWLockRelease(MultiXactGenLock);

	debug_elog6(DEBUG2,
				"MultiXact: checkpoint is nextMulti %u, nextOffset %u, oldestMulti %u in DB %u",
				*nextMulti, *nextMultiOffset, *oldestMulti, *oldestMultiDB);
}

/*
 * 执行检查点 --- 无论是在关闭期间还是动态执行
 */
void CheckPointMultiXact(void)
{
	TRACE_POSTGRESQL_MULTIXACT_CHECKPOINT_START(true);

	/*
	 * 将脏的MultiXact页面写入磁盘。这可能导致同步请求
	 * 排队，以便稍后由ProcessSyncRequests()处理，作为
	 * 检查点的一部分。
	 */
	SimpleLruWriteAll(MultiXactOffsetCtl, true);
	SimpleLruWriteAll(MultiXactMemberCtl, true);

	TRACE_POSTGRESQL_MULTIXACT_CHECKPOINT_DONE(true);
}

/*
 * 设置下一个要分配的MultiXactId和offset
 *
 * 当我们能够准确确定来自检查点记录的正确下一个ID/offset时使用。
 * 尽管这仅在引导和XLog重放期间被调用，我们仍然获取锁，以防
 * 任何热备份后端正在检查这些值。
 */
void MultiXactSetNextMXact(MultiXactId fc_nextMulti,
					  MultiXactOffset fc_nextMultiOffset)
{
	debug_elog4(DEBUG2, "MultiXact: setting next multi to %u offset %u",
				nextMulti, nextMultiOffset);
	LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
	MultiXactState->nextMXact = fc_nextMulti;
	MultiXactState->nextOffset = fc_nextMultiOffset;
	LWLockRelease(MultiXactGenLock);

	/*
	 * 在二进制升级期间，确保offset SLRU足够大
	 * 以容纳将要创建的下一个值。
	 *
	 * 我们需要在二进制升级模式下的第一次启动过程中相当早地
	 * 做到这一点：实际上在StartupMultiXact()之前，因为这个例程
	 * 实际上在StartupXLOG()之前被调用。而且我们不能在这个
	 * 时间点之前做到这一点，因为在这个例程的第一次调用中，
	 * 我们确定了MultiXactState->nextMXact值， 
	 * 这正是MaybeExtendOffsetSlru所需要的。
	 */
	if (IsBinaryUpgrade)
		fc_MaybeExtendOffsetSlru();
}

/*
 * 确定给定当前最旧的 datminmxid（即，可能在我们集群的任何数据库中存在的最旧的 MultiXactId）和具有该值的数据库的 OID 的最后一个安全的 MultiXactId 的分配。
 *
 * 当我们刚启动集群时，is_startup 为 true；在运行中的集群中更新状态时，is_startup 为 false。这仅影响日志消息。
 */
void SetMultiXactIdLimit(MultiXactId fc_oldest_datminmxid, Oid fc_oldest_datoid,
					bool fc_is_startup)
{
	MultiXactId fc_multiVacLimit;
	MultiXactId fc_multiWarnLimit;
	MultiXactId fc_multiStopLimit;
	MultiXactId fc_multiWrapLimit;
	MultiXactId fc_curMulti;
	bool		fc_needs_offset_vacuum;

	Assert(MultiXactIdIsValid(fc_oldest_datminmxid));

	/*
	 * 我们假装多重事务 ID 空间的中间会发生包装，但这并不是真的，因为多重事务的包装方式与事务 ID 不同。请注意，除了对多重事务 ID 包装的任何担忧，我们还必须确保多重事务成员不会包装。限值在 SetOffsetVacuumLimit 中设置，而不是在这里。
	 */
	fc_multiWrapLimit = fc_oldest_datminmxid + (MaxMultiXactId >> 1);
	if (fc_multiWrapLimit < FirstMultiXactId)
		fc_multiWrapLimit += FirstMultiXactId;

	/*
	 * 一旦我们接近 300 万个数据丢失的多重事务 ID，我们将拒绝继续分配 MultiXactIds。参见 SetTransactionIdLimit。
	 */
	fc_multiStopLimit = fc_multiWrapLimit - 3000000;
	if (fc_multiStopLimit < FirstMultiXactId)
		fc_multiStopLimit -= FirstMultiXactId;

	/*
	 * 一旦我们接近 4000 万个数据丢失的多重事务 ID，我们将开始大声抱怨。这有点任意，但如果你的油表降到 2% 满，你会寻找下一个加油站吗？我们需要对这个数字相当自由，因为有很多场景大多数事务是由不关注警告的自动客户端完成的。（不，我们不会将其配置化。如果你知道足够去配置它，你就足够知道不要陷入这种麻烦。）
	 */
	fc_multiWarnLimit = fc_multiWrapLimit - 40000000;
	if (fc_multiWarnLimit < FirstMultiXactId)
		fc_multiWarnLimit -= FirstMultiXactId;

	/*
	 * 当 oldest_datminmxid 超过 autovacuum_multixact_freeze_max_age mxids 时，我们将开始尝试强制自动清理。
	 *
	 * 注：autovacuum_multixact_freeze_max_age 是一个 PGC_POSTMASTER 参数，以便我们不必担心在其值的即时变化中处理。参见 SetTransactionIdLimit。
	 */
	fc_multiVacLimit = fc_oldest_datminmxid + autovacuum_multixact_freeze_max_age;
	if (fc_multiVacLimit < FirstMultiXactId)
		fc_multiVacLimit += FirstMultiXactId;

	/* 为新的限制值设置锁定足够长的时间 */
	LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
	MultiXactState->oldestMultiXactId = fc_oldest_datminmxid;
	MultiXactState->oldestMultiXactDB = fc_oldest_datoid;
	MultiXactState->multiVacLimit = fc_multiVacLimit;
	MultiXactState->multiWarnLimit = fc_multiWarnLimit;
	MultiXactState->multiStopLimit = fc_multiStopLimit;
	MultiXactState->multiWrapLimit = fc_multiWrapLimit;
	fc_curMulti = MultiXactState->nextMXact;
	LWLockRelease(MultiXactGenLock);

	/* 记录信息 */
	ereport(DEBUG1,
			(errmsg_internal("MultiXactId wrap limit is %u, limited by database with OID %u",
							 fc_multiWrapLimit, fc_oldest_datoid)));

	/*
	 * 只有在数据目录处于一致状态时，才能计算实际的限制。在仍然重播 WAL 时没有必要计算限制——即使可能重播多重事务的创建，也不会对新的多重事务做出决定。因此，我们只会在调用 TrimMultiXact() 后进行进一步检查。
	 */
	if (!MultiXactState->finishedStartup)
		return;

	Assert(!InRecovery);

	/* 设置偏移清理的限制。 */
	fc_needs_offset_vacuum = fc_SetOffsetVacuumLimit(fc_is_startup);

	/*
	 * 如果超过自动清理的强制点，立即发出自动清理请求。其原因是自动清理一次只处理一个数据库。一旦它完成了对最旧数据库的清理，它会在这里调用我们，如果还有其他旧数据库存在，我们将立即通知主进程开始另一个迭代。
	 */
	if ((MultiXactIdPrecedes(fc_multiVacLimit, fc_curMulti) ||
		 fc_needs_offset_vacuum) && IsUnderPostmaster)
		SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_LAUNCHER);

	/* 如果超过包装警告点立即发出警告 */
	if (MultiXactIdPrecedes(fc_multiWarnLimit, fc_curMulti))
	{
		char	   *fc_oldest_datname;

		/*
		 * 我们可以在不在事务内部时被调用，例如在
		 * StartupXLOG()期间。在这种情况下，我们不能进行数据库访问，
		 * 所以我们必须仅报告最旧数据库的OID。
		 *
		 * 注意：get_database_name也可能失败并返回NULL，
		 * 例如因为数据库刚被删除。我们仍然会警告，尽管这个警告可能现在是不必要的。
		 */
		if (IsTransactionState())
			fc_oldest_datname = get_database_name(fc_oldest_datoid);
		else
			fc_oldest_datname = NULL;

		if (fc_oldest_datname)
			ereport(WARNING,
					(errmsg_plural("database \"%s\" must be vacuumed before %u more MultiXactId is used",
								   "database \"%s\" must be vacuumed before %u more MultiXactIds are used",
								   fc_multiWrapLimit - fc_curMulti,
								   fc_oldest_datname,
								   fc_multiWrapLimit - fc_curMulti),
					 errhint("To avoid a database shutdown, execute a database-wide VACUUM in that database.\n"
							 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
		else
			ereport(WARNING,
					(errmsg_plural("database with OID %u must be vacuumed before %u more MultiXactId is used",
								   "database with OID %u must be vacuumed before %u more MultiXactIds are used",
								   fc_multiWrapLimit - fc_curMulti,
								   fc_oldest_datoid,
								   fc_multiWrapLimit - fc_curMulti),
					 errhint("To avoid a database shutdown, execute a database-wide VACUUM in that database.\n"
							 "You might also need to commit or roll back old prepared transactions, or drop stale replication slots.")));
	}
}

/*
 * 确保下一个待分配的MultiXactId至少为minMulti，
 * 同样，nextOffset至少为minMultiOffset。
 *
 * 当我们可以从XLog记录中确定安全的最小值时使用
 * （无论是在线检查点还是mxact创建日志条目）。
 * 尽管这仅在XLog重放期间调用，我们会加锁以防
 * 任何热备用后端正在检查这些值。
 */
void MultiXactAdvanceNextMXact(MultiXactId fc_minMulti,
						  MultiXactOffset fc_minMultiOffset)
{
	LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
	if (MultiXactIdPrecedes(MultiXactState->nextMXact, fc_minMulti))
	{
		debug_elog3(DEBUG2, "MultiXact: setting next multi to %u", minMulti);
		MultiXactState->nextMXact = fc_minMulti;
	}
	if (fc_MultiXactOffsetPrecedes(MultiXactState->nextOffset, fc_minMultiOffset))
	{
		debug_elog3(DEBUG2, "MultiXact: setting next offset to %u",
					minMultiOffset);
		MultiXactState->nextOffset = fc_minMultiOffset;
	}
	LWLockRelease(MultiXactGenLock);
}

/*
 * 更新我们的oldestMultiXactId值，但仅当它比我们拥有的
 * 更近时。
 *
 * 这可能仅在WAL重放期间调用。
 */
void MultiXactAdvanceOldest(MultiXactId fc_oldestMulti, Oid fc_oldestMultiDB)
{
	Assert(InRecovery);

	if (MultiXactIdPrecedes(MultiXactState->oldestMultiXactId, fc_oldestMulti))
		SetMultiXactIdLimit(fc_oldestMulti, fc_oldestMultiDB, false);
}

/*
 * 确保MultiXactOffset有空间容纳新分配的MultiXactId。
 *
 * 注意：在持有MultiXactGenLock时调用此方法。
 * 我们希望大多数时候它运行得很快；即使当它不那么快时，
 * 除非我们被迫写出脏日志或xlog页面以腾出共享内存，
 * 否则实际的I/O不会发生。
 */
static void fc_ExtendMultiXactOffset(MultiXactId fc_multi)
{
	int			fc_pageno;

	/*
	 * 只有在页面的第一个MultiXactId时才有工作。
	 * 但请注意：在包围后，页面零的第一个MultiXactId是FirstMultiXactId。
	 */
	if (MultiXactIdToOffsetEntry(fc_multi) != 0 &&
		fc_multi != FirstMultiXactId)
		return;

	fc_pageno = MultiXactIdToOffsetPage(fc_multi);

	LWLockAcquire(MultiXactOffsetSLRULock, LW_EXCLUSIVE);

	/* 将页面置零并创建一个关于它的 XLOG 条目 */
	fc_ZeroMultiXactOffsetPage(fc_pageno, true);

	LWLockRelease(MultiXactOffsetSLRULock);
}

/*
 * 确保MultiXactMember有空间容纳新分配的MultiXactId的成员。
 *
 * 与上述例程一样，这在持有MultiXactGenLock时调用；
 * 相同的评论适用。
 */
static void fc_ExtendMultiXactMember(MultiXactOffset fc_offset, int fc_nmembers)
{
	/*
	 * 可能成员跨越了成员文件的多个页面，因此我们循环以确保
	 * 考虑每个页面。如果成员跨越多个页面，编码并不
	 * 最优，但这似乎足够不寻常，无需过于担心。
	 */
	while (fc_nmembers > 0)
	{
		int			fc_flagsoff;
		int			fc_flagsbit;
		uint32		fc_difference;

		/*
		 * 仅在页面的第一个条目时清零。
		 */
		fc_flagsoff = MXOffsetToFlagsOffset(fc_offset);
		fc_flagsbit = MXOffsetToFlagsBitShift(fc_offset);
		if (fc_flagsoff == 0 && fc_flagsbit == 0)
		{
			int			fc_pageno;

			fc_pageno = MXOffsetToMemberPage(fc_offset);

			LWLockAcquire(MultiXactMemberSLRULock, LW_EXCLUSIVE);

			/* 将页面置零并创建一个关于它的 XLOG 条目 */
			fc_ZeroMultiXactMemberPage(fc_pageno, true);

			LWLockRelease(MultiXactMemberSLRULock);
		}

		/*
		 * 计算到当前页面末尾的项目数量。小心：如果
		 * 无符号整数的加法出现溢出，我们就在最后一个
		 * 段的最后一页；由于该页面持有与其他页面不同的
		 * 项目数量，我们需要不同处理。
		 */
		if (fc_offset + MAX_MEMBERS_IN_LAST_MEMBERS_PAGE < fc_offset)
		{
			/*
			 * 这是最后一个段的最后一页；我们可以计算
			 * 在其中留下的项目数量，而无需模
			 * 算术。
			 */
			fc_difference = MaxMultiXactOffset - fc_offset + 1;
		}
		else
			fc_difference = MULTIXACT_MEMBERS_PER_PAGE - fc_offset % MULTIXACT_MEMBERS_PER_PAGE;

		/*
		 * 向下一个页面推进，注意正确处理包围
		 * 情况。如果nmembers变为负数也没关系。
		 */
		fc_nmembers -= fc_difference;
		fc_offset += fc_difference;
	}
}

/*
 * GetOldestMultiXactId
 *
 * 返回仍可能被任何正在运行的事务视为活动的最旧的 MultiXactId。较旧的可能仍存在于磁盘上，但它们不再有任何正在运行的成员事务。
 *
 * 在此函数返回的值上截断 MultiXact SLRU 段是不安全的；但是，它可以被完整表清理用来设定可能截断该表 SLRU 的点。
 */
MultiXactId GetOldestMultiXactId(void)
{
	MultiXactId fc_oldestMXact;
	MultiXactId fc_nextMXact;
	int			fc_i;

	/*
	 * 这是所有 OldestMemberMXactId[] 和 OldestVisibleMXactId[] 条目中最旧的有效值，或者如果没有有效值则为 nextMXact。
	 */
	LWLockAcquire(MultiXactGenLock, LW_SHARED);

	/*
	 * 我们必须小心 nextMXact 可能处于环绕状态。我们在这里并不修复计数器本身，但必须确保在计算中使用有效值。
	 */
	fc_nextMXact = MultiXactState->nextMXact;
	if (fc_nextMXact < FirstMultiXactId)
		fc_nextMXact = FirstMultiXactId;

	fc_oldestMXact = fc_nextMXact;
	for (fc_i = 1; fc_i <= MaxOldestSlot; fc_i++)
	{
		MultiXactId fc_thisoldest;

		fc_thisoldest = OldestMemberMXactId[fc_i];
		if (MultiXactIdIsValid(fc_thisoldest) &&
			MultiXactIdPrecedes(fc_thisoldest, fc_oldestMXact))
			fc_oldestMXact = fc_thisoldest;
		fc_thisoldest = OldestVisibleMXactId[fc_i];
		if (MultiXactIdIsValid(fc_thisoldest) &&
			MultiXactIdPrecedes(fc_thisoldest, fc_oldestMXact))
			fc_oldestMXact = fc_thisoldest;
	}

	LWLockRelease(MultiXactGenLock);

	return fc_oldestMXact;
}

/*
 * 确定我们需要多么积极地进行清理，以防止成员环绕。
 *
 * 为此，确定最旧的成员偏移，并在 MultiXactState 中安装限制信息，从而可以用于防止成员 SLRU 区域中过时数据的溢出。
 *
 * 如果需要紧急自动清理，则返回值为 true；否则返回 false。
 */
static bool fc_SetOffsetVacuumLimit(bool fc_is_startup)
{
	MultiXactId fc_oldestMultiXactId;
	MultiXactId fc_nextMXact;
	MultiXactOffset fc_oldestOffset = 0;	/* 安抚编译器 */
	MultiXactOffset fc_prevOldestOffset;
	MultiXactOffset fc_nextOffset;
	bool		fc_oldestOffsetKnown = false;
	bool		fc_prevOldestOffsetKnown;
	MultiXactOffset fc_offsetStopLimit = 0;
	MultiXactOffset fc_prevOffsetStopLimit;

	/*
	 * 注意：必须防止并发截断，否则我们可能会尝试查找一个正在被截断的 oldestMulti。
	 */
	LWLockAcquire(MultiXactTruncationLock, LW_SHARED);

	/* 从共享内存中读取相关字段。 */
	LWLockAcquire(MultiXactGenLock, LW_SHARED);
	fc_oldestMultiXactId = MultiXactState->oldestMultiXactId;
	fc_nextMXact = MultiXactState->nextMXact;
	fc_nextOffset = MultiXactState->nextOffset;
	fc_prevOldestOffsetKnown = MultiXactState->oldestOffsetKnown;
	fc_prevOldestOffset = MultiXactState->oldestOffset;
	fc_prevOffsetStopLimit = MultiXactState->offsetStopLimit;
	Assert(MultiXactState->finishedStartup);
	LWLockRelease(MultiXactGenLock);

	/*
	 * 确定最旧 multixact 的偏移。通常，我们可以从 multixact 本身读取偏移，但有一个重要的特殊情况：如果根本不存在任何 multixact，oldestMXact 显然无法指向一个。它将指向在需要 multixact 时下次分配的 multixact ID。
	 */
	if (fc_oldestMultiXactId == fc_nextMXact)
	{
		/*
		 * 当下一个 multixact 被创建时，它将存储在下一个偏移处。
		 */
		fc_oldestOffset = fc_nextOffset;
		fc_oldestOffsetKnown = true;
	}
	else
	{
		/*
		 * 查明最旧的现有 multixact 的偏移存储在哪里。由于 PostgreSQL 9.3.X 和 9.4.X 早期版本中的错误，所谓的最早 multixact 可能并不存在。在这种情况下，我们会小心不出现失败。
		 */
		fc_oldestOffsetKnown =
			fc_find_multixact_start(fc_oldestMultiXactId, &fc_oldestOffset);

		if (fc_oldestOffsetKnown)
			ereport(DEBUG1,
					(errmsg_internal("oldest MultiXactId member is at offset %u",
									 fc_oldestOffset)));
		else
			ereport(LOG,
					(errmsg("MultiXact member wraparound protections are disabled because oldest checkpointed MultiXact %u does not exist on disk",
							fc_oldestMultiXactId)));
	}

	LWLockRelease(MultiXactTruncationLock);

	/*
	 * 如果可以，计算限制（并将其安装在 MultiXactState 中），以防止成员 SLRU 区域中过时数据的溢出。然而，我们只有在已知最旧偏移时才能这样做。
	 */
	if (fc_oldestOffsetKnown)
	{
		/* 移回相应段的开头 */
		fc_offsetStopLimit = fc_oldestOffset - (fc_oldestOffset %
										  (MULTIXACT_MEMBERS_PER_PAGE * SLRU_PAGES_PER_SEGMENT));

		/* 始终在环绕点之前留下一段 */
		fc_offsetStopLimit -= (MULTIXACT_MEMBERS_PER_PAGE * SLRU_PAGES_PER_SEGMENT);

		if (!fc_prevOldestOffsetKnown && !fc_is_startup)
			ereport(LOG,
					(errmsg("MultiXact member wraparound protections are now enabled")));

		ereport(DEBUG1,
				(errmsg_internal("MultiXact member stop limit is now %u based on MultiXact %u",
								 fc_offsetStopLimit, fc_oldestMultiXactId)));
	}
	else if (fc_prevOldestOffsetKnown)
	{
		/*
		 * 如果这次未能获得最旧的偏移，但我们有来自上一次通过此函数的值，则使用旧值，而不是再次自动强制进行紧急自动清理周期。
		 */
		fc_oldestOffset = fc_prevOldestOffset;
		fc_oldestOffsetKnown = true;
		fc_offsetStopLimit = fc_prevOffsetStopLimit;
	}

	/* 安装计算出的值 */
	LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
	MultiXactState->oldestOffset = fc_oldestOffset;
	MultiXactState->oldestOffsetKnown = fc_oldestOffsetKnown;
	MultiXactState->offsetStopLimit = fc_offsetStopLimit;
	LWLockRelease(MultiXactGenLock);

	/*
	 * 我们需要紧急自动清理吗？如果不确定，则假设是的。
	 */
	return !fc_oldestOffsetKnown ||
		(fc_nextOffset - fc_oldestOffset > MULTIXACT_MEMBER_SAFE_THRESHOLD);
}

/*
 * 返回在“start”上添加“distance”是否会超过“boundary”。
 *
 * 我们用此来确定加法是否在“boundary”点周围“包裹”，
 * 因此有了这个名称。我们不想在这里使用常规的
 * 2^31-modulo 算法的原因是，我们希望能够使用整个
 * 2^32-1 的空间，从而允许比其他情况下更多的 multixacts。
 */
static bool fc_MultiXactOffsetWouldWrap(MultiXactOffset fc_boundary, MultiXactOffset fc_start,
						 uint32 fc_distance)
{
	MultiXactOffset fc_finish;

	/*
	 * 请注意，偏移量 0 没有使用（参见 GetMultiXactIdMembers），因此
	 * 如果加法在 UINT_MAX 边界上环绕，请跳过该值。
	 */
	fc_finish = fc_start + fc_distance;
	if (fc_finish < fc_start)
		fc_finish++;

	/*-----------------------------------------------------------------------
	 * 当边界在数值上大于起始点时，两个之间的任何数值
	 * 都没有被包裹：
	 *
	 *	<----S----B---->
	 *	[---)			 = F 拖拽过 B（和 UINT_MAX）
	 *		 [---)		 = F 没有被包裹
	 *			  [----] = F 拖拽过 B
	 *
	 * 当边界在数值上小于起始点（即 UINT_MAX 环绕发生在
	 * 之间的某个地方）时，所有中间的值都被包裹：
	 *
	 *	<----B----S---->
	 *	[---)			 = F 没有拖拽过 B（但是拖拽过 UINT_MAX）
	 *		 [---)		 = F 拖拽过 B（和 UINT_MAX）
	 *			  [----] = F 没有被包裹
	 *-----------------------------------------------------------------------
	 */
	if (fc_start < fc_boundary)
		return fc_finish >= fc_boundary || fc_finish < fc_start;
	else
		return fc_finish >= fc_boundary && fc_finish < fc_start;
}

/*
 * 找到给定 MultiXactId 的起始偏移量。
 *
 * 如果包含多重数据的文件在磁盘上不存在，则返回 false。
 * 否则，返回 true，并将 *result 设置为起始成员偏移量。
 *
 * 此函数不防止并发截断，因此如果需要，
 * 调用者需要对此进行保护。
 */
static bool fc_find_multixact_start(MultiXactId fc_multi, MultiXactOffset *fc_result)
{
	MultiXactOffset fc_offset;
	int			fc_pageno;
	int			fc_entryno;
	int			fc_slotno;
	MultiXactOffset *fc_offptr;

	Assert(MultiXactState->finishedStartup);

	fc_pageno = MultiXactIdToOffsetPage(fc_multi);
	fc_entryno = MultiXactIdToOffsetEntry(fc_multi);

	/*
	 * 写出脏数据，以便 PhysicalPageExists 可以正确工作。
	 */
	SimpleLruWriteAll(MultiXactOffsetCtl, true);
	SimpleLruWriteAll(MultiXactMemberCtl, true);

	if (!SimpleLruDoesPhysicalPageExist(MultiXactOffsetCtl, fc_pageno))
		return false;

	/* 锁由 SimpleLruReadPage_ReadOnly 获取 */
	fc_slotno = SimpleLruReadPage_ReadOnly(MultiXactOffsetCtl, fc_pageno, fc_multi);
	fc_offptr = (MultiXactOffset *) MultiXactOffsetCtl->shared->page_buffer[fc_slotno];
	fc_offptr += fc_entryno;
	fc_offset = *fc_offptr;
	LWLockRelease(MultiXactOffsetSLRULock);

	*fc_result = fc_offset;
	return true;
}

/*
 * 确定当前存在多少 multixacts，以及有多少 multixact 成员。
 * 如果无法确定，则返回 false。
 */
static bool fc_ReadMultiXactCounts(uint32 *fc_multixacts, MultiXactOffset *fc_members)
{
	MultiXactOffset fc_nextOffset;
	MultiXactOffset fc_oldestOffset;
	MultiXactId fc_oldestMultiXactId;
	MultiXactId fc_nextMultiXactId;
	bool		fc_oldestOffsetKnown;

	LWLockAcquire(MultiXactGenLock, LW_SHARED);
	fc_nextOffset = MultiXactState->nextOffset;
	fc_oldestMultiXactId = MultiXactState->oldestMultiXactId;
	fc_nextMultiXactId = MultiXactState->nextMXact;
	fc_oldestOffset = MultiXactState->oldestOffset;
	fc_oldestOffsetKnown = MultiXactState->oldestOffsetKnown;
	LWLockRelease(MultiXactGenLock);

	if (!fc_oldestOffsetKnown)
		return false;

	*fc_members = fc_nextOffset - fc_oldestOffset;
	*fc_multixacts = fc_nextMultiXactId - fc_oldestMultiXactId;
	return true;
}

/*
 * 多事务成员可以在引用它们的多事务比每个 datminmxid 都旧时被移除。autovacuum_multixact_freeze_max_age 和 vacuum_multixact_freeze_table_age 一起工作，以确保我们永远不会有太多的多事务；我们希望，在正常情况下，这也足以让我们不使用太多的偏移量。但是，如果平均多事务有很多成员，我们可能会在仍然使用相对少量成员的情况下耗尽成员空间，以至于这些限制无法触发完整的表扫描以推进 relminmxid。在那时，我们别无选择，只能开始使创建多事务操作失败并报错。
 *
 * 为了防止这种情况，如果使用的成员空间超过某个阈值，我们实际上会将 autovacuum_multixact_freeze_max_age 减少到一个小于正在使用的多事务数量的值。我们希望这会迅速触发拥有最旧 relminmxid 的表的自动清理，从而允许 datminmxid 值推进并移除一些成员。
 *
 * 随着当前使用的成员空间比例的增加，我们在限制该值时变得更加积极。这不仅导致自动清理加速，而且还使用户手动发出的任何清理更加积极。这是因为 vacuum_set_xid_limits() 根据该函数返回的有效 autovacuum_multixact_freeze_max_age 限制冻结表和最低冻结年龄。在最坏的情况下，我们将冻结最大年龄声明为零，任何表的每次清理都会尝试冻结每个多事务。
 *
 * 这些阈值可能应该允许用户调节，但目前我们保持简单。
 */
int MultiXactMemberFreezeThreshold(void)
{
	MultiXactOffset fc_members;
	uint32		fc_multixacts;
	uint32		fc_victim_multixacts;
	double		fc_fraction;
	int			fc_result;

	/* 如果我们无法确定成员空间的利用率，则假设最坏情况。 */
	if (!fc_ReadMultiXactCounts(&fc_multixacts, &fc_members))
		return 0;

	/* 如果成员空间利用率低，则不需要采取特殊措施。 */
	if (fc_members <= MULTIXACT_MEMBER_SAFE_THRESHOLD)
		return autovacuum_multixact_freeze_max_age;

	/*
	 * 计算 relminmxid 推进的目标。我们尝试从系统中消除的多事务数量
	 * 基于我们超过 MULTIXACT_MEMBER_SAFE_THRESHOLD 的程度。
	 */
	fc_fraction = (double) (fc_members - MULTIXACT_MEMBER_SAFE_THRESHOLD) /
		(MULTIXACT_MEMBER_DANGER_THRESHOLD - MULTIXACT_MEMBER_SAFE_THRESHOLD);
	fc_victim_multixacts = fc_multixacts * fc_fraction;

	/* 比例可能大于 1.0，但最低可能的冻结年龄为零 */
	if (fc_victim_multixacts > fc_multixacts)
		return 0;
	fc_result = fc_multixacts - fc_victim_multixacts;

	/*
	 * 限制为 autovacuum_multixact_freeze_max_age，以确保我们不会使
	 * 自动清理的攻击性减弱。
	 */
	return Min(fc_result, autovacuum_multixact_freeze_max_age);
}

typedef struct mxtruncinfo
{
	int			earliestExistingPage;
} mxtruncinfo;

/*
 * SlruScanDirectory 回调
 *		该回调确定最早存在的页面编号。
 */
static bool fc_SlruScanDirCbFindEarliest(SlruCtl fc_ctl, char *fc_filename, int fc_segpage, void *fc_data)
{
	mxtruncinfo *fc_trunc = (mxtruncinfo *) fc_data;

	if (fc_trunc->earliestExistingPage == -1 ||
		fc_ctl->PagePrecedes(fc_segpage, fc_trunc->earliestExistingPage))
	{
		fc_trunc->earliestExistingPage = fc_segpage;
	}

	return false;				/* 继续进行 */
}


/*
 * 删除成员片段 [oldest, newOldest)
 *
 * 成员 SLRU 与偏移量 SLRU 相比，可以几乎一次填充到全部范围。
 * 这意味着 SimpleLruTruncate() 不能轻易使用 - 而是使用偏移量
 * SLRU 计算要删除的范围。参见 TruncateMultiXact()。
 */
static void fc_PerformMembersTruncation(MultiXactOffset fc_oldestOffset, MultiXactOffset fc_newOldestOffset)
{
	const int	fc_maxsegment = MXOffsetToMemberSegment(MaxMultiXactOffset);
	int			fc_startsegment = MXOffsetToMemberSegment(fc_oldestOffset);
	int			fc_endsegment = MXOffsetToMemberSegment(fc_newOldestOffset);
	int			fc_segment = fc_startsegment;

	/*
	 * 删除所有片段，但保留最后一个。最后一个片段仍然可以
	 * 包含有效数据，可能是部分有效。
	 */
	while (fc_segment != fc_endsegment)
	{
		elog(DEBUG2, "truncating multixact members segment %x", fc_segment);
		SlruDeleteSegment(MultiXactMemberCtl, fc_segment);

		/* 移动到下一个片段，正确处理环绕 */
		if (fc_segment == fc_maxsegment)
			fc_segment = 0;
		else
			fc_segment += 1;
	}
}

/*
 * 删除偏移量段 [oldest, newOldest)
 */
static void fc_PerformOffsetsTruncation(MultiXactId fc_oldestMulti, MultiXactId fc_newOldestMulti)
{
	/*
	 * 我们退回一个 multixact，以避免在极少数情况下 oldestMulti 会是
	 * 页面上的第一个项且 oldestMulti == nextMulti 的情况下经过尚未创建的
	 * 截止页面。 在这种情况下，如果我们不减去一，我们将触发 SimpleLruTruncate
	 * 的环绕检测。
	 */
	SimpleLruTruncate(MultiXactOffsetCtl,
					  MultiXactIdToOffsetPage(PreviousMultiXactId(fc_newOldestMulti)));
}

/*
 * 移除所有在仍然有兴趣的最旧的 MultiXactOffset 和 MultiXactMember 段之前的段。
 *
 * 这仅在主节点上作为 vacuum 的一部分被调用 (通过
 * vac_truncate_clog())。在恢复时，通过重放在此记录的截断 WAL 记录进行截断。
 *
 * newOldestMulti 是当前所需的最旧 multixact，newOldestMultiDB
 * 是防止 newOldestMulti 增加的数据库之一。
 */
void TruncateMultiXact(MultiXactId fc_newOldestMulti, Oid fc_newOldestMultiDB)
{
	MultiXactId fc_oldestMulti;
	MultiXactId fc_nextMulti;
	MultiXactOffset fc_newOldestOffset;
	MultiXactOffset fc_oldestOffset;
	MultiXactOffset fc_nextOffset;
	mxtruncinfo fc_trunc;
	MultiXactId fc_earliest;

	Assert(!RecoveryInProgress());
	Assert(MultiXactState->finishedStartup);

	/*
	 * 我们只能允许一次进行一个截断。否则成员的某些部分可能会在我们
	 * 进行查找或类似操作时消失。创建新 multis 等不需要互锁，因为这些
	 * 是受到限制(只能增长，不能缩小)。
	 */
	LWLockAcquire(MultiXactTruncationLock, LW_EXCLUSIVE);

	LWLockAcquire(MultiXactGenLock, LW_SHARED);
	fc_nextMulti = MultiXactState->nextMXact;
	fc_nextOffset = MultiXactState->nextOffset;
	fc_oldestMulti = MultiXactState->oldestMultiXactId;
	LWLockRelease(MultiXactGenLock);
	Assert(MultiXactIdIsValid(fc_oldestMulti));

	/*
	 * 确保仅在有需要截断的值时尝试截断。在正常处理情况下，值不应
	 * 后退，但由于错误存在一些特殊情况，可能会出现这种情况。
	 */
	if (MultiXactIdPrecedesOrEquals(fc_newOldestMulti, fc_oldestMulti))
	{
		LWLockRelease(MultiXactTruncationLock);
		return;
	}

	/*
	 * 请注意，我们不能在没有 segments 可截断的情况下继续进行截断；可能
	 * 会没有 segments 可截断，这将是一个问题，因为我们将尝试读取偏移量
	 * 页面以确定在哪里截断成员 SLRU。因此，我们首先扫描目录以确定
	 * 我们可以无错误读取的最早的偏移量页面编号。
	 *
	 * 当 nextMXact 离 multiWrapLimit 还不到一个段时，
	 * SlruScanDirCbFindEarliest 可能找到一些早期的段，而不是实际的最早段。
	 * (MultiXactOffsetPagePrecedes(EARLIEST, LATEST)
	 * 返回 false，因为并非所有项对具有相同的答案。)
	 * 当早期的截断尝试未能进行 unlink() 或提前从此函数返回时，
	 * 这种情况也可能发生。唯一的后果是提前返回，这浪费了我们本可以释放的空间。
	 *
	 * 注意：最旧的多重事务所在的页面也可能已经被截断，我们在更新
	 * oldestMulti 之前崩溃了。
	 */
	fc_trunc.earliestExistingPage = -1;
	SlruScanDirectory(MultiXactOffsetCtl, fc_SlruScanDirCbFindEarliest, &fc_trunc);
	fc_earliest = fc_trunc.earliestExistingPage * MULTIXACT_OFFSETS_PER_PAGE;
	if (fc_earliest < FirstMultiXactId)
		fc_earliest = FirstMultiXactId;

	/* 如果没有任何内容可移除，我们可以提前退出。 */
	if (MultiXactIdPrecedes(fc_oldestMulti, fc_earliest))
	{
		LWLockRelease(MultiXactTruncationLock);
		return;
	}

	/*
	 * 首先，计算 MultiXactMember 的安全截断点。这是最旧的 multixact 的
	 * 起始偏移量。
	 *
	 * 希望 find_multixact_start 在这里始终有效，因为我们已经检查过它不
	 * 会早于磁盘上的最早 MultiXact。但是如果它失败了，就不要截断任何内容，并记录一条信息。
	 */
	if (fc_oldestMulti == fc_nextMulti)
	{
		/* 没有 MultiXacts */
		fc_oldestOffset = fc_nextOffset;
	}
	else if (!fc_find_multixact_start(fc_oldestMulti, &fc_oldestOffset))
	{
		ereport(LOG,
				(errmsg("oldest MultiXact %u not found, earliest MultiXact %u, skipping truncation",
						fc_oldestMulti, fc_earliest)));
		LWLockRelease(MultiXactTruncationLock);
		return;
	}

	/*
	 * 第二，计算截断到何处。为此查找对应于 newOldestMulti 的成员偏移量。
	 */
	if (fc_newOldestMulti == fc_nextMulti)
	{
		/* 没有 MultiXacts */
		fc_newOldestOffset = fc_nextOffset;
	}
	else if (!fc_find_multixact_start(fc_newOldestMulti, &fc_newOldestOffset))
	{
		ereport(LOG,
				(errmsg("cannot truncate up to MultiXact %u because it does not exist on disk, skipping truncation",
						fc_newOldestMulti)));
		LWLockRelease(MultiXactTruncationLock);
		return;
	}

	elog(DEBUG1, "performing multixact truncation: "
		 "offsets [%u, %u), offsets segments [%x, %x), "
		 "members [%u, %u), members segments [%x, %x)",
		 fc_oldestMulti, fc_newOldestMulti,
		 MultiXactIdToOffsetSegment(fc_oldestMulti),
		 MultiXactIdToOffsetSegment(fc_newOldestMulti),
		 fc_oldestOffset, fc_newOldestOffset,
		 MXOffsetToMemberSegment(fc_oldestOffset),
		 MXOffsetToMemberSegment(fc_newOldestOffset));

	/*
	 * 在关键区域内进行截断和WAL记录的截断。这样偏移量/成员就不会失去同步，即一旦一致，newOldestMulti将始终存在于成员中，即使我们在错误的时刻崩溃。
	 */
	START_CRIT_SECTION();

	/*
	 * 防止检查点被同时调度。这是至关重要的，因为否则截断记录在崩溃/基础备份后可能不会被重放，即使数据目录的状态要求它。
	 */
	Assert((MyProc->delayChkptFlags & DELAY_CHKPT_START) == 0);
	MyProc->delayChkptFlags |= DELAY_CHKPT_START;

	/* WAL日志截断 */
	fc_WriteMTruncateXlogRec(fc_newOldestMultiDB,
						  fc_oldestMulti, fc_newOldestMulti,
						  fc_oldestOffset, fc_newOldestOffset);

	/*
	 * 在执行截断之前，在关键区域内更新内存限制：必须在截断之前进行，以防止对这些值的并发查找。必须在关键区域内，否则未来对该函数的调用将在查找偏移量中的最旧成员时出错，如果我们的调用者在更新限制之前崩溃。
	 */
	LWLockAcquire(MultiXactGenLock, LW_EXCLUSIVE);
	MultiXactState->oldestMultiXactId = fc_newOldestMulti;
	MultiXactState->oldestMultiXactDB = fc_newOldestMultiDB;
	LWLockRelease(MultiXactGenLock);

	/* 首先截断成员 */
	fc_PerformMembersTruncation(fc_oldestOffset, fc_newOldestOffset);

	/* 然后截断偏移量 */
	fc_PerformOffsetsTruncation(fc_oldestMulti, fc_newOldestMulti);

	MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;

	END_CRIT_SECTION();
	LWLockRelease(MultiXactTruncationLock);
}

/*
 * 决定一个MultiXactOffset页号在截断目的上是否“更旧”。类似于CLOGPagePrecedes()。
 *
 * 偏移这些值是可选的，因为MultiXactIdPrecedes()具有转换对称性。
 */
static bool fc_MultiXactOffsetPagePrecedes(int fc_page1, int fc_page2)
{
	MultiXactId fc_multi1;
	MultiXactId fc_multi2;

	fc_multi1 = ((MultiXactId) fc_page1) * MULTIXACT_OFFSETS_PER_PAGE;
	fc_multi1 += FirstMultiXactId + 1;
	fc_multi2 = ((MultiXactId) fc_page2) * MULTIXACT_OFFSETS_PER_PAGE;
	fc_multi2 += FirstMultiXactId + 1;

	return (MultiXactIdPrecedes(fc_multi1, fc_multi2) &&
			MultiXactIdPrecedes(fc_multi1,
								fc_multi2 + MULTIXACT_OFFSETS_PER_PAGE - 1));
}

/*
 * 决定一个MultiXactMember页号在截断目的上是否“更旧”。没有“无效偏移量”，所以逐字使用这些数字。
 */
static bool fc_MultiXactMemberPagePrecedes(int fc_page1, int fc_page2)
{
	MultiXactOffset fc_offset1;
	MultiXactOffset fc_offset2;

	fc_offset1 = ((MultiXactOffset) fc_page1) * MULTIXACT_MEMBERS_PER_PAGE;
	fc_offset2 = ((MultiXactOffset) fc_page2) * MULTIXACT_MEMBERS_PER_PAGE;

	return (fc_MultiXactOffsetPrecedes(fc_offset1, fc_offset2) &&
			fc_MultiXactOffsetPrecedes(fc_offset1,
									fc_offset2 + MULTIXACT_MEMBERS_PER_PAGE - 1));
}

/*
 * 决定两个MultiXactIds中哪个更早。
 *
 * XXX 我们需要为InvalidMultiXactId做些什么特别的事情吗？
 * （看起来不需要。）
 */
bool MultiXactIdPrecedes(MultiXactId fc_multi1, MultiXactId fc_multi2)
{
	int32		fc_diff = (int32) (fc_multi1 - fc_multi2);

	return (fc_diff < 0);
}

/*
 * MultiXactIdPrecedesOrEquals -- multi1在逻辑上是否<= multi2？
 *
 * XXX 我们需要为InvalidMultiXactId做些什么特别的事情吗？
 * （看起来不需要。）
 */
bool MultiXactIdPrecedesOrEquals(MultiXactId fc_multi1, MultiXactId fc_multi2)
{
	int32		fc_diff = (int32) (fc_multi1 - fc_multi2);

	return (fc_diff <= 0);
}


/*
 * 决定两个偏移量中哪个更早。
 */
static bool fc_MultiXactOffsetPrecedes(MultiXactOffset fc_offset1, MultiXactOffset fc_offset2)
{
	int32		fc_diff = (int32) (fc_offset1 - fc_offset2);

	return (fc_diff < 0);
}

/*
 * 写入一个xlog记录，反映MEMBERs或OFFSETs页面的零值（信息显示哪个）
 */
static void fc_WriteMZeroPageXlogRec(int fc_pageno, uint8 fc_info)
{
	XLogBeginInsert();
	XLogRegisterData((char *) (&fc_pageno), sizeof(int));
	(void) XLogInsert(RM_MULTIXACT_ID, fc_info);
}

/*
 * 写入一个TRUNCATE xlog记录
 *
 * 在返回之前，我们必须将xlog记录刷新到磁盘 --- 请参见TruncateCLOG()中的备注。
 */
static void fc_WriteMTruncateXlogRec(Oid fc_oldestMultiDB,
					  MultiXactId fc_startTruncOff, MultiXactId fc_endTruncOff,
					  MultiXactOffset fc_startTruncMemb, MultiXactOffset fc_endTruncMemb)
{
	XLogRecPtr	fc_recptr;
	xl_multixact_truncate fc_xlrec;

	fc_xlrec.oldestMultiDB = fc_oldestMultiDB;

	fc_xlrec.startTruncOff = fc_startTruncOff;
	fc_xlrec.endTruncOff = fc_endTruncOff;

	fc_xlrec.startTruncMemb = fc_startTruncMemb;
	fc_xlrec.endTruncMemb = fc_endTruncMemb;

	XLogBeginInsert();
	XLogRegisterData((char *) (&fc_xlrec), SizeOfMultiXactTruncate);
	fc_recptr = XLogInsert(RM_MULTIXACT_ID, XLOG_MULTIXACT_TRUNCATE_ID);
	XLogFlush(fc_recptr);
}

/*
 * MULTIXACT资源管理器的例程
 */
void multixact_redo(XLogReaderState *fc_record)
{
	uint8		fc_info = XLogRecGetInfo(fc_record) & ~XLR_INFO_MASK;

	/* 备份块在multixact记录中不被使用 */
	Assert(!XLogRecHasAnyBlockRefs(fc_record));

	if (fc_info == XLOG_MULTIXACT_ZERO_OFF_PAGE)
	{
		int			fc_pageno;
		int			fc_slotno;

		memcpy(&fc_pageno, XLogRecGetData(fc_record), sizeof(int));

		LWLockAcquire(MultiXactOffsetSLRULock, LW_EXCLUSIVE);

		fc_slotno = fc_ZeroMultiXactOffsetPage(fc_pageno, false);
		SimpleLruWritePage(MultiXactOffsetCtl, fc_slotno);
		Assert(!MultiXactOffsetCtl->shared->page_dirty[fc_slotno]);

		LWLockRelease(MultiXactOffsetSLRULock);
	}
	else if (fc_info == XLOG_MULTIXACT_ZERO_MEM_PAGE)
	{
		int			fc_pageno;
		int			fc_slotno;

		memcpy(&fc_pageno, XLogRecGetData(fc_record), sizeof(int));

		LWLockAcquire(MultiXactMemberSLRULock, LW_EXCLUSIVE);

		fc_slotno = fc_ZeroMultiXactMemberPage(fc_pageno, false);
		SimpleLruWritePage(MultiXactMemberCtl, fc_slotno);
		Assert(!MultiXactMemberCtl->shared->page_dirty[fc_slotno]);

		LWLockRelease(MultiXactMemberSLRULock);
	}
	else if (fc_info == XLOG_MULTIXACT_CREATE_ID)
	{
		xl_multixact_create *fc_xlrec =
		(xl_multixact_create *) XLogRecGetData(fc_record);
		TransactionId fc_max_xid;
		int			fc_i;

		/* 将数据存储回到SLRU文件中 */
		fc_RecordNewMultiXact(fc_xlrec->mid, fc_xlrec->moff, fc_xlrec->nmembers,
						   fc_xlrec->members);

		/* 确保nextMXact/nextOffset在此记录具有的内容之外 */
		MultiXactAdvanceNextMXact(fc_xlrec->mid + 1,
								  fc_xlrec->moff + fc_xlrec->nmembers);

		/*
		 * 确保nextXid超出记录中提到的任何XID。这应该是不必要的，因为在这里发现的任何XID应该在XLOG中具有其他证据，但我们还是要安全起见。
		 */
		fc_max_xid = XLogRecGetXid(fc_record);
		for (fc_i = 0; fc_i < fc_xlrec->nmembers; fc_i++)
		{
			if (TransactionIdPrecedes(fc_max_xid, fc_xlrec->members[fc_i].xid))
				fc_max_xid = fc_xlrec->members[fc_i].xid;
		}

		AdvanceNextFullTransactionIdPastXid(fc_max_xid);
	}
	else if (fc_info == XLOG_MULTIXACT_TRUNCATE_ID)
	{
		xl_multixact_truncate fc_xlrec;
		int			fc_pageno;

		memcpy(&fc_xlrec, XLogRecGetData(fc_record),
			   SizeOfMultiXactTruncate);

		elog(DEBUG1, "replaying multixact truncation: "
			 "offsets [%u, %u), offsets segments [%x, %x), "
			 "members [%u, %u), members segments [%x, %x)",
			 fc_xlrec.startTruncOff, fc_xlrec.endTruncOff,
			 MultiXactIdToOffsetSegment(fc_xlrec.startTruncOff),
			 MultiXactIdToOffsetSegment(fc_xlrec.endTruncOff),
			 fc_xlrec.startTruncMemb, fc_xlrec.endTruncMemb,
			 MXOffsetToMemberSegment(fc_xlrec.startTruncMemb),
			 MXOffsetToMemberSegment(fc_xlrec.endTruncMemb));

		/* 应该不是必需的，但足够便宜 */
		LWLockAcquire(MultiXactTruncationLock, LW_EXCLUSIVE);

		/*
		 * 前进地平线值，以便在恢复结束时它们是最新的。
		 */
		SetMultiXactIdLimit(fc_xlrec.endTruncOff, fc_xlrec.oldestMultiDB, false);

		fc_PerformMembersTruncation(fc_xlrec.startTruncMemb, fc_xlrec.endTruncMemb);

		/*
		 * 在XLOG重放期间，latest_page_number不一定已经设置；插入合适的值以绕过SimpleLruTruncate中的完整性测试。
		 */
		fc_pageno = MultiXactIdToOffsetPage(fc_xlrec.endTruncOff);
		MultiXactOffsetCtl->shared->latest_page_number = fc_pageno;
		fc_PerformOffsetsTruncation(fc_xlrec.startTruncOff, fc_xlrec.endTruncOff);

		LWLockRelease(MultiXactTruncationLock);
	}
	else
		elog(PANIC, "multixact_redo: unknown op code %u", fc_info);
}

Datum pg_get_multixact_members(PG_FUNCTION_ARGS)
{
	typedef struct
	{
		MultiXactMember *members;
		int			nmembers;
		int			iter;
	} mxact;
	MultiXactId fc_mxid = PG_GETARG_TRANSACTIONID(0);
	mxact	   *fc_multi;
	FuncCallContext *fc_funccxt;

	if (fc_mxid < FirstMultiXactId)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("invalid MultiXactId: %u", fc_mxid)));

	if (SRF_IS_FIRSTCALL())
	{
		MemoryContext fc_oldcxt;
		TupleDesc	fc_tupdesc;

		fc_funccxt = SRF_FIRSTCALL_INIT();
		fc_oldcxt = MemoryContextSwitchTo(fc_funccxt->multi_call_memory_ctx);

		fc_multi = palloc(sizeof(mxact));
		/* 在这里不需要考虑旧值 */
		fc_multi->nmembers = GetMultiXactIdMembers(fc_mxid, &fc_multi->members, false,
												false);
		fc_multi->iter = 0;

		fc_tupdesc = CreateTemplateTupleDesc(2);
		TupleDescInitEntry(fc_tupdesc, (AttrNumber) 1, "xid",
						   XIDOID, -1, 0);
		TupleDescInitEntry(fc_tupdesc, (AttrNumber) 2, "mode",
						   TEXTOID, -1, 0);

		fc_funccxt->attinmeta = TupleDescGetAttInMetadata(fc_tupdesc);
		fc_funccxt->user_fctx = fc_multi;

		MemoryContextSwitchTo(fc_oldcxt);
	}

	fc_funccxt = SRF_PERCALL_SETUP();
	fc_multi = (mxact *) fc_funccxt->user_fctx;

	while (fc_multi->iter < fc_multi->nmembers)
	{
		HeapTuple	fc_tuple;
		char	   *fc_values[2];

		fc_values[0] = psprintf("%u", fc_multi->members[fc_multi->iter].xid);
		fc_values[1] = fc_mxstatus_to_string(fc_multi->members[fc_multi->iter].status);

		fc_tuple = BuildTupleFromCStrings(fc_funccxt->attinmeta, fc_values);

		fc_multi->iter++;
		pfree(fc_values[0]);
		SRF_RETURN_NEXT(fc_funccxt, HeapTupleGetDatum(fc_tuple));
	}

	SRF_RETURN_DONE(fc_funccxt);
}

/*
 * sync.c的入口点，用于同步偏移量文件。
 */
int multixactoffsetssyncfiletag(const FileTag *fc_ftag, char *fc_path)
{
	return SlruSyncFileTag(MultiXactOffsetCtl, fc_ftag, fc_path);
}

/*
 * sync.c的入口点，用于同步成员文件。
 */
int multixactmemberssyncfiletag(const FileTag *fc_ftag, char *fc_path)
{
	return SlruSyncFileTag(MultiXactMemberCtl, fc_ftag, fc_path);
}
